/*  _____ _
 * |_   _| |_  _ _ ___ ___ _ __  __ _
 *   | | | ' \| '_/ -_) -_) '  \/ _` |_
 *   |_| |_||_|_| \___\___|_|_|_\__,_(_)
 *
 * Threema for Android
 * Copyright (c) 2019-2020 Threema GmbH
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

/*
 * Based on AOSP code with modifications by Threema
 *
 * https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
 *
 * (C) Google Inc. All Rights Reserved
 * Licensed under the Apache License, version 2.0
 */

package ch.threema.app.utils;

import android.annotation.TargetApi;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.location.Location;
import android.media.MediaDataSource;
import android.media.MediaMetadataRetriever;
import android.os.Build;
import android.util.Log;
import android.util.Pair;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;

import static ch.threema.client.Utils.byteArrayToHexString;
import static java.nio.charset.StandardCharsets.US_ASCII;

/**
 * This is a class for reading and writing Exif tags in a JPEG file or a RAW image file.
 * <p>
 * Supported formats are: JPEG, DNG, CR2, NEF, NRW, ARW, RW2, ORF, PEF, SRW, RAF and HEIF.
 * <p>
 * Attribute mutation is supported for JPEG image files.
 */
public class ExifInterface {
	private static final Logger logger = LoggerFactory.getLogger(ExifInterface.class);

	private static final boolean DEBUG = false;

	// The Exif tag names. See JEITA CP-3451C specifications (Exif 2.3) Section 3-8.
	// A. Tags related to image data structure
	/**
	 *  <p>The number of columns of image data, equal to the number of pixels per row. In JPEG
	 *  compressed data, this tag shall not be used because a JPEG marker is used instead of it.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 256</li>
	 *      <li>Type = Unsigned short or Unsigned long</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_IMAGE_WIDTH = "ImageWidth";
	/**
	 *  <p>The number of rows of image data. In JPEG compressed data, this tag shall not be used
	 *  because a JPEG marker is used instead of it.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 257</li>
	 *      <li>Type = Unsigned short or Unsigned long</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_IMAGE_LENGTH = "ImageLength";
	/**
	 *  <p>The number of bits per image component. In this standard each component of the image is
	 *  8 bits, so the value for this tag is 8. See also {@link #TAG_SAMPLES_PER_PIXEL}. In JPEG
	 *  compressed data, this tag shall not be used because a JPEG marker is used instead of it.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 258</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 3</li>
	 *      <li>Default = {@link #BITS_PER_SAMPLE_RGB}</li>
	 *  </ul>
	 */
	public static final String TAG_BITS_PER_SAMPLE = "BitsPerSample";
	/**
	 *  <p>The compression scheme used for the image data. When a primary image is JPEG compressed,
	 *  this designation is not necessary. So, this tag shall not be recorded. When thumbnails use
	 *  JPEG compression, this tag value is set to 6.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 259</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #DATA_UNCOMPRESSED
	 *  @see #DATA_JPEG
	 */
	public static final String TAG_COMPRESSION = "Compression";
	/**
	 *  <p>The pixel composition. In JPEG compressed data, this tag shall not be used because a JPEG
	 *  marker is used instead of it.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 262</li>
	 *      <li>Type = SHORT</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #PHOTOMETRIC_INTERPRETATION_RGB
	 *  @see #PHOTOMETRIC_INTERPRETATION_YCBCR
	 */
	public static final String TAG_PHOTOMETRIC_INTERPRETATION = "PhotometricInterpretation";
	/**
	 *  <p>The image orientation viewed in terms of rows and columns.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 274</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = {@link #ORIENTATION_NORMAL}</li>
	 *  </ul>
	 *
	 *  @see #ORIENTATION_UNDEFINED
	 *  @see #ORIENTATION_NORMAL
	 *  @see #ORIENTATION_FLIP_HORIZONTAL
	 *  @see #ORIENTATION_ROTATE_180
	 *  @see #ORIENTATION_FLIP_VERTICAL
	 *  @see #ORIENTATION_TRANSPOSE
	 *  @see #ORIENTATION_ROTATE_90
	 *  @see #ORIENTATION_TRANSVERSE
	 *  @see #ORIENTATION_ROTATE_270
	 */
	public static final String TAG_ORIENTATION = "Orientation";
	/**
	 *  <p>The number of components per pixel. Since this standard applies to RGB and YCbCr images,
	 *  the value set for this tag is 3. In JPEG compressed data, this tag shall not be used because
	 *  a JPEG marker is used instead of it.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 277</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = 3</li>
	 *  </ul>
	 */
	public static final String TAG_SAMPLES_PER_PIXEL = "SamplesPerPixel";
	/**
	 *  <p>Indicates whether pixel components are recorded in chunky or planar format. In JPEG
	 *  compressed data, this tag shall not be used because a JPEG marker is used instead of it.
	 *  If this field does not exist, the TIFF default, {@link #FORMAT_CHUNKY}, is assumed.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 284</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *  </ul>
	 *
	 *  @see #FORMAT_CHUNKY
	 *  @see #FORMAT_PLANAR
	 */
	public static final String TAG_PLANAR_CONFIGURATION = "PlanarConfiguration";
	/**
	 *  <p>The sampling ratio of chrominance components in relation to the luminance component.
	 *  In JPEG compressed data a JPEG marker is used instead of this tag. So, this tag shall not
	 *  be recorded.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 530</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 2</li>
	 *      <ul>
	 *          <li>[2, 1] = YCbCr4:2:2</li>
	 *          <li>[2, 2] = YCbCr4:2:0</li>
	 *          <li>Other = reserved</li>
	 *      </ul>
	 *  </ul>
	 */
	public static final String TAG_Y_CB_CR_SUB_SAMPLING = "YCbCrSubSampling";
	/**
	 *  <p>The position of chrominance components in relation to the luminance component. This field
	 *  is designated only for JPEG compressed data or uncompressed YCbCr data. The TIFF default is
	 *  {@link #Y_CB_CR_POSITIONING_CENTERED}; but when Y:Cb:Cr = 4:2:2 it is recommended in this
	 *  standard that {@link #Y_CB_CR_POSITIONING_CO_SITED} be used to record data, in order to
	 *  improve the image quality when viewed on TV systems. When this field does not exist,
	 *  the reader shall assume the TIFF default. In the case of Y:Cb:Cr = 4:2:0, the TIFF default
	 *  ({@link #Y_CB_CR_POSITIONING_CENTERED}) is recommended. If the Exif/DCF reader does not
	 *  have the capability of supporting both kinds of positioning, it shall follow the TIFF
	 *  default regardless of the value in this field. It is preferable that readers can support
	 *  both centered and co-sited positioning.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 531</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = {@link #Y_CB_CR_POSITIONING_CENTERED}</li>
	 *  </ul>
	 *
	 *  @see #Y_CB_CR_POSITIONING_CENTERED
	 *  @see #Y_CB_CR_POSITIONING_CO_SITED
	 */
	public static final String TAG_Y_CB_CR_POSITIONING = "YCbCrPositioning";
	/**
	 *  <p>The number of pixels per {@link #TAG_RESOLUTION_UNIT} in the {@link #TAG_IMAGE_WIDTH}
	 *  direction. When the image resolution is unknown, 72 [dpi] shall be designated.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 282</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = 72</li>
	 *  </ul>
	 *
	 *  @see #TAG_Y_RESOLUTION
	 *  @see #TAG_RESOLUTION_UNIT
	 */
	public static final String TAG_X_RESOLUTION = "XResolution";
	/**
	 *  <p>The number of pixels per {@link #TAG_RESOLUTION_UNIT} in the {@link #TAG_IMAGE_WIDTH}
	 *  direction. The same value as {@link #TAG_X_RESOLUTION} shall be designated.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 283</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = 72</li>
	 *  </ul>
	 *
	 *  @see #TAG_X_RESOLUTION
	 *  @see #TAG_RESOLUTION_UNIT
	 */
	public static final String TAG_Y_RESOLUTION = "YResolution";
	/**
	 *  <p>The unit for measuring {@link #TAG_X_RESOLUTION} and {@link #TAG_Y_RESOLUTION}. The same
	 *  unit is used for both {@link #TAG_X_RESOLUTION} and {@link #TAG_Y_RESOLUTION}. If the image
	 *  resolution is unknown, {@link #RESOLUTION_UNIT_INCHES} shall be designated.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 296</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = {@link #RESOLUTION_UNIT_INCHES}</li>
	 *  </ul>
	 *
	 *  @see #RESOLUTION_UNIT_INCHES
	 *  @see #RESOLUTION_UNIT_CENTIMETERS
	 *  @see #TAG_X_RESOLUTION
	 *  @see #TAG_Y_RESOLUTION
	 */
	public static final String TAG_RESOLUTION_UNIT = "ResolutionUnit";

	// B. Tags related to recording offset
	/**
	 *  <p>For each strip, the byte offset of that strip. It is recommended that this be selected
	 *  so the number of strip bytes does not exceed 64 KBytes.In the case of JPEG compressed data,
	 *  this designation is not necessary. So, this tag shall not be recorded.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 273</li>
	 *      <li>Type = Unsigned short or Unsigned long</li>
	 *      <li>Count = StripsPerImage (for {@link #FORMAT_CHUNKY})
	 *               or {@link #TAG_SAMPLES_PER_PIXEL} * StripsPerImage
	 *               (for {@link #FORMAT_PLANAR})</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  <p>StripsPerImage = floor(({@link #TAG_IMAGE_LENGTH} + {@link #TAG_ROWS_PER_STRIP} - 1)
	 *  / {@link #TAG_ROWS_PER_STRIP})</p>
	 *
	 *  @see #TAG_ROWS_PER_STRIP
	 *  @see #TAG_STRIP_BYTE_COUNTS
	 */
	public static final String TAG_STRIP_OFFSETS = "StripOffsets";
	/**
	 *  <p>The number of rows per strip. This is the number of rows in the image of one strip when
	 *  an image is divided into strips. In the case of JPEG compressed data, this designation is
	 *  not necessary. So, this tag shall not be recorded.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 278</li>
	 *      <li>Type = Unsigned short or Unsigned long</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #TAG_STRIP_OFFSETS
	 *  @see #TAG_STRIP_BYTE_COUNTS
	 */
	public static final String TAG_ROWS_PER_STRIP = "RowsPerStrip";
	/**
	 *  <p>The total number of bytes in each strip. In the case of JPEG compressed data, this
	 *  designation is not necessary. So, this tag shall not be recorded.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 279</li>
	 *      <li>Type = Unsigned short or Unsigned long</li>
	 *      <li>Count = StripsPerImage (when using {@link #FORMAT_CHUNKY})
	 *               or {@link #TAG_SAMPLES_PER_PIXEL} * StripsPerImage
	 *               (when using {@link #FORMAT_PLANAR})</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  <p>StripsPerImage = floor(({@link #TAG_IMAGE_LENGTH} + {@link #TAG_ROWS_PER_STRIP} - 1)
	 *  / {@link #TAG_ROWS_PER_STRIP})</p>
	 */
	public static final String TAG_STRIP_BYTE_COUNTS = "StripByteCounts";
	/**
	 *  <p>The offset to the start byte (SOI) of JPEG compressed thumbnail data. This shall not be
	 *  used for primary image JPEG data.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 513</li>
	 *      <li>Type = Unsigned long</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_JPEG_INTERCHANGE_FORMAT = "JPEGInterchangeFormat";
	/**
	 *  <p>The number of bytes of JPEG compressed thumbnail data. This is not used for primary image
	 *  JPEG data. JPEG thumbnails are not divided but are recorded as a continuous JPEG bitstream
	 *  from SOI to EOI. APPn and COM markers should not be recorded. Compressed thumbnails shall be
	 *  recorded in no more than 64 KBytes, including all other data to be recorded in APP1.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 514</li>
	 *      <li>Type = Unsigned long</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = "JPEGInterchangeFormatLength";

	// C. Tags related to Image Data Characteristics
	/**
	 *  <p>A transfer function for the image, described in tabular style. Normally this tag need not
	 *  be used, since color space is specified in {@link #TAG_COLOR_SPACE}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 301</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 3 * 256</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_TRANSFER_FUNCTION = "TransferFunction";
	/**
	 *  <p>The chromaticity of the white point of the image. Normally this tag need not be used,
	 *  since color space is specified in {@link #TAG_COLOR_SPACE}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 318</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 2</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_WHITE_POINT = "WhitePoint";
	/**
	 *  <p>The chromaticity of the three primary colors of the image. Normally this tag need not
	 *  be used, since color space is specified in {@link #TAG_COLOR_SPACE}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 319</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 6</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_PRIMARY_CHROMATICITIES = "PrimaryChromaticities";
	/**
	 *  <p>The matrix coefficients for transformation from RGB to YCbCr image data. About
	 *  the default value, please refer to JEITA CP-3451C Spec, Annex D.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 529</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 3</li>
	 *  </ul>
	 */
	public static final String TAG_Y_CB_CR_COEFFICIENTS = "YCbCrCoefficients";
	/**
	 *  <p>The reference black point value and reference white point value. No defaults are given
	 *  in TIFF, but the values below are given as defaults here. The color space is declared in
	 *  a color space information tag, with the default being the value that gives the optimal image
	 *  characteristics Interoperability these conditions</p>
	 *
	 *  <ul>
	 *      <li>Tag = 532</li>
	 *      <li>Type = RATIONAL</li>
	 *      <li>Count = 6</li>
	 *      <li>Default = [0, 255, 0, 255, 0, 255] (when {@link #TAG_PHOTOMETRIC_INTERPRETATION}
	 *                 is {@link #PHOTOMETRIC_INTERPRETATION_RGB})
	 *                 or [0, 255, 0, 128, 0, 128] (when {@link #TAG_PHOTOMETRIC_INTERPRETATION}
	 *                 is {@link #PHOTOMETRIC_INTERPRETATION_YCBCR})</li>
	 *  </ul>
	 */
	public static final String TAG_REFERENCE_BLACK_WHITE = "ReferenceBlackWhite";

	// D. Other tags
	/**
	 *  <p>The date and time of image creation. In this standard it is the date and time the file
	 *  was changed. The format is "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and
	 *  the date and time separated by one blank character ({@code 0x20}). When the date and time
	 *  are unknown, all the character spaces except colons (":") should be filled with blank
	 *  characters, or else the Interoperability field should be filled with blank characters.
	 *  The character string length is 20 Bytes including NULL for termination. When the field is
	 *  left blank, it is treated as unknown.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 306</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 19</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_DATETIME = "DateTime";
	/**
	 *  <p>An ASCII string giving the title of the image. It is possible to be added a comment
	 *  such as "1988 company picnic" or the like. Two-byte character codes cannot be used. When
	 *  a 2-byte code is necessary, {@link #TAG_USER_COMMENT} is to be used.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 270</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_IMAGE_DESCRIPTION = "ImageDescription";
	/**
	 *  <p>The manufacturer of the recording equipment. This is the manufacturer of the DSC,
	 *  scanner, video digitizer or other equipment that generated the image. When the field is left
	 *  blank, it is treated as unknown.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 271</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_MAKE = "Make";
	/**
	 *  <p>The model name or model number of the equipment. This is the model name of number of
	 *  the DSC, scanner, video digitizer or other equipment that generated the image. When
	 *  the field is left blank, it is treated as unknown.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 272</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_MODEL = "Model";
	/**
	 *  <p>This tag records the name and version of the software or firmware of the camera or image
	 *  input device used to generate the image. The detailed format is not specified, but it is
	 *  recommended that the example shown below be followed. When the field is left blank, it is
	 *  treated as unknown.</p>
	 *
	 *  <p>Ex.) "Exif Software Version 1.00a".</p>
	 *
	 *  <ul>
	 *      <li>Tag = 305</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_SOFTWARE = "Software";
	/**
	 *  <p>This tag records the name of the camera owner, photographer or image creator.
	 *  The detailed format is not specified, but it is recommended that the information be written
	 *  as in the example below for ease of Interoperability. When the field is left blank, it is
	 *  treated as unknown.</p>
	 *
	 *  <p>Ex.) "Camera owner, John Smith; Photographer, Michael Brown; Image creator,
	 *  Ken James"</p>
	 *
	 *  <ul>
	 *      <li>Tag = 315</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_ARTIST = "Artist";
	/**
	 *  <p>Copyright information. In this standard the tag is used to indicate both the photographer
	 *  and editor copyrights. It is the copyright notice of the person or organization claiming
	 *  rights to the image. The Interoperability copyright statement including date and rights
	 *  should be written in this field; e.g., "Copyright, John Smith, 19xx. All rights reserved."
	 *  In this standard the field records both the photographer and editor copyrights, with each
	 *  recorded in a separate part of the statement. When there is a clear distinction between
	 *  the photographer and editor copyrights, these are to be written in the order of photographer
	 *  followed by editor copyright, separated by NULL (in this case, since the statement also ends
	 *  with a NULL, there are two NULL codes) (see example 1). When only the photographer copyright
	 *  is given, it is terminated by one NULL code (see example 2). When only the editor copyright
	 *  is given, the photographer copyright part consists of one space followed by a terminating
	 *  NULL code, then the editor copyright is given (see example 3). When the field is left blank,
	 *  it is treated as unknown.</p>
	 *
	 *  <p>Ex. 1) When both the photographer copyright and editor copyright are given.
	 *  <ul><li>Photographer copyright + NULL + editor copyright + NULL</li></ul></p>
	 *  <p>Ex. 2) When only the photographer copyright is given.
	 *  <ul><li>Photographer copyright + NULL</li></ul></p>
	 *  <p>Ex. 3) When only the editor copyright is given.
	 *  <ul><li>Space ({@code 0x20}) + NULL + editor copyright + NULL</li></ul></p>
	 *
	 *  <ul>
	 *      <li>Tag = 315</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_COPYRIGHT = "Copyright";

	// Exif IFD Attribute Information
	// A. Tags related to version
	/**
	 *  <p>The version of this standard supported. Nonexistence of this field is taken to mean
	 *  nonconformance to the standard. In according with conformance to this standard, this tag
	 *  shall be recorded like "0230” as 4-byte ASCII.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 36864</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Length = 4</li>
	 *      <li>Default = "0230"</li>
	 *  </ul>
	 */
	public static final String TAG_EXIF_VERSION = "ExifVersion";
	/**
	 *  <p>The Flashpix format version supported by a FPXR file. If the FPXR function supports
	 *  Flashpix format Ver. 1.0, this is indicated similarly to {@link #TAG_EXIF_VERSION} by
	 *  recording "0100" as 4-byte ASCII.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 40960</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Length = 4</li>
	 *      <li>Default = "0100"</li>
	 *  </ul>
	 */
	public static final String TAG_FLASHPIX_VERSION = "FlashpixVersion";

	// B. Tags related to image data characteristics
	/**
	 *  <p>The color space information tag is always recorded as the color space specifier.
	 *  Normally {@link #COLOR_SPACE_S_RGB} is used to define the color space based on the PC
	 *  monitor conditions and environment. If a color space other than {@link #COLOR_SPACE_S_RGB}
	 *  is used, {@link #COLOR_SPACE_UNCALIBRATED} is set. Image data recorded as
	 *  {@link #COLOR_SPACE_UNCALIBRATED} may be treated as {@link #COLOR_SPACE_S_RGB} when it is
	 *  converted to Flashpix.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 40961</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *  </ul>
	 *
	 *  @see #COLOR_SPACE_S_RGB
	 *  @see #COLOR_SPACE_UNCALIBRATED
	 */
	public static final String TAG_COLOR_SPACE = "ColorSpace";
	/**
	 *  <p>Indicates the value of coefficient gamma. The formula of transfer function used for image
	 *  reproduction is expressed as follows.</p>
	 *
	 *  <p>(Reproduced value) = (Input value) ^ gamma</p>
	 *
	 *  <p>Both reproduced value and input value indicate normalized value, whose minimum value is
	 *  0 and maximum value is 1.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 42240</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GAMMA = "Gamma";

	// C. Tags related to image configuration
	/**
	 *  <p>Information specific to compressed data. When a compressed file is recorded, the valid
	 *  width of the meaningful image shall be recorded in this tag, whether or not there is padding
	 *  data or a restart marker. This tag shall not exist in an uncompressed file.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 40962</li>
	 *      <li>Type = Unsigned short or Unsigned long</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_PIXEL_X_DIMENSION = "PixelXDimension";
	/**
	 *  <p>Information specific to compressed data. When a compressed file is recorded, the valid
	 *  height of the meaningful image shall be recorded in this tag, whether or not there is
	 *  padding data or a restart marker. This tag shall not exist in an uncompressed file.
	 *  Since data padding is unnecessary in the vertical direction, the number of lines recorded
	 *  in this valid image height tag will in fact be the same as that recorded in the SOF.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 40963</li>
	 *      <li>Type = Unsigned short or Unsigned long</li>
	 *      <li>Count = 1</li>
	 *  </ul>
	 */
	public static final String TAG_PIXEL_Y_DIMENSION = "PixelYDimension";
	/**
	 *  <p>Information specific to compressed data. The channels of each component are arranged
	 *  in order from the 1st component to the 4th. For uncompressed data the data arrangement is
	 *  given in the {@link #TAG_PHOTOMETRIC_INTERPRETATION}. However, since
	 *  {@link #TAG_PHOTOMETRIC_INTERPRETATION} can only express the order of Y, Cb and Cr, this tag
	 *  is provided for cases when compressed data uses components other than Y, Cb, and Cr and to
	 *  enable support of other sequences.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37121</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Length = 4</li>
	 *      <li>Default = 4 5 6 0 (if RGB uncompressed) or 1 2 3 0 (other cases)</li>
	 *      <ul>
	 *          <li>0 = does not exist</li>
	 *          <li>1 = Y</li>
	 *          <li>2 = Cb</li>
	 *          <li>3 = Cr</li>
	 *          <li>4 = R</li>
	 *          <li>5 = G</li>
	 *          <li>6 = B</li>
	 *          <li>other = reserved</li>
	 *      </ul>
	 *  </ul>
	 */
	public static final String TAG_COMPONENTS_CONFIGURATION = "ComponentsConfiguration";
	/**
	 *  <p>Information specific to compressed data. The compression mode used for a compressed image
	 *  is indicated in unit bits per pixel.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37122</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_COMPRESSED_BITS_PER_PIXEL = "CompressedBitsPerPixel";

	// D. Tags related to user information
	/**
	 *  <p>A tag for manufacturers of Exif/DCF writers to record any desired information.
	 *  The contents are up to the manufacturer, but this tag shall not be used for any other than
	 *  its intended purpose.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37500</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_MAKER_NOTE = "MakerNote";
	/**
	 *  <p>A tag for Exif users to write keywords or comments on the image besides those in
	 *  {@link #TAG_IMAGE_DESCRIPTION}, and without the character code limitations of it.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37510</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_USER_COMMENT = "UserComment";

	// E. Tags related to related file information
	/**
	 *  <p>This tag is used to record the name of an audio file related to the image data. The only
	 *  relational information recorded here is the Exif audio file name and extension (an ASCII
	 *  string consisting of 8 characters + '.' + 3 characters). The path is not recorded.</p>
	 *
	 *  <p>When using this tag, audio files shall be recorded in conformance to the Exif audio
	 *  format. Writers can also store the data such as Audio within APP2 as Flashpix extension
	 *  stream data. Audio files shall be recorded in conformance to the Exif audio format.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 40964</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 12</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_RELATED_SOUND_FILE = "RelatedSoundFile";

	// F. Tags related to date and time
	/**
	 *  <p>The date and time when the original image data was generated. For a DSC the date and time
	 *  the picture was taken are recorded. The format is "YYYY:MM:DD HH:MM:SS" with time shown in
	 *  24-hour format, and the date and time separated by one blank character ({@code 0x20}).
	 *  When the date and time are unknown, all the character spaces except colons (":") should be
	 *  filled with blank characters, or else the Interoperability field should be filled with blank
	 *  characters. When the field is left blank, it is treated as unknown.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 36867</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 19</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_DATETIME_ORIGINAL = "DateTimeOriginal";
	/**
	 *  <p>The date and time when the image was stored as digital data. If, for example, an image
	 *  was captured by DSC and at the same time the file was recorded, then
	 *  {@link #TAG_DATETIME_ORIGINAL} and this tag will have the same contents. The format is
	 *  "YYYY:MM:DD HH:MM:SS" with time shown in 24-hour format, and the date and time separated by
	 *  one blank character ({@code 0x20}). When the date and time are unknown, all the character
	 *  spaces except colons (":")should be filled with blank characters, or else
	 *  the Interoperability field should be filled with blank characters. When the field is left
	 *  blank, it is treated as unknown.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 36868</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 19</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_DATETIME_DIGITIZED = "DateTimeDigitized";
	/**
	 *  <p>A tag used to record fractions of seconds for {@link #TAG_DATETIME}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37520</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_SUBSEC_TIME = "SubSecTime";
	/**
	 *  <p>A tag used to record fractions of seconds for {@link #TAG_DATETIME_ORIGINAL}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37521</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_SUBSEC_TIME_ORIGINAL = "SubSecTimeOriginal";
	/**
	 *  <p>A tag used to record fractions of seconds for {@link #TAG_DATETIME_DIGITIZED}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37522</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_SUBSEC_TIME_DIGITIZED = "SubSecTimeDigitized";

	// G. Tags related to picture-taking condition
	/**
	 *  <p>Exposure time, given in seconds.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 33434</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_EXPOSURE_TIME = "ExposureTime";
	/**
	 *  <p>The F number.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 33437</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_F_NUMBER = "FNumber";
	/**
	 *  <p>TThe class of the program used by the camera to set exposure when the picture is taken.
	 *  The tag values are as follows.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 34850</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = {@link #EXPOSURE_PROGRAM_NOT_DEFINED}</li>
	 *  </ul>
	 *
	 *  @see #EXPOSURE_PROGRAM_NOT_DEFINED
	 *  @see #EXPOSURE_PROGRAM_MANUAL
	 *  @see #EXPOSURE_PROGRAM_NORMAL
	 *  @see #EXPOSURE_PROGRAM_APERTURE_PRIORITY
	 *  @see #EXPOSURE_PROGRAM_SHUTTER_PRIORITY
	 *  @see #EXPOSURE_PROGRAM_CREATIVE
	 *  @see #EXPOSURE_PROGRAM_ACTION
	 *  @see #EXPOSURE_PROGRAM_PORTRAIT_MODE
	 *  @see #EXPOSURE_PROGRAM_LANDSCAPE_MODE
	 */
	public static final String TAG_EXPOSURE_PROGRAM = "ExposureProgram";
	/**
	 *  <p>Indicates the spectral sensitivity of each channel of the camera used. The tag value is
	 *  an ASCII string compatible with the standard developed by the ASTM Technical committee.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 34852</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_SPECTRAL_SENSITIVITY = "SpectralSensitivity";
	/**
	 *  @deprecated Use {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} instead.
	 *  @see #TAG_PHOTOGRAPHIC_SENSITIVITY
	 */
	@Deprecated public static final String TAG_ISO_SPEED_RATINGS = "ISOSpeedRatings";
	/**
	 *  <p>This tag indicates the sensitivity of the camera or input device when the image was shot.
	 *  More specifically, it indicates one of the following values that are parameters defined in
	 *  ISO 12232: standard output sensitivity (SOS), recommended exposure index (REI), or ISO
	 *  speed. Accordingly, if a tag corresponding to a parameter that is designated by
	 *  {@link #TAG_SENSITIVITY_TYPE} is recorded, the values of the tag and of this tag are
	 *  the same. However, if the value is 65535 or higher, the value of this tag shall be 65535.
	 *  When recording this tag, {@link #TAG_SENSITIVITY_TYPE} should also be recorded. In addition,
	 *  while “Count = Any”, only 1 count should be used when recording this tag.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 34855</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = Any</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_PHOTOGRAPHIC_SENSITIVITY = "PhotographicSensitivity";
	/**
	 *  <p>Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524. OECF is
	 *  the relationship between the camera optical input and the image values.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 34856</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_OECF = "OECF";
	/**
	 *  <p>This tag indicates which one of the parameters of ISO12232 is
	 *  {@link #TAG_PHOTOGRAPHIC_SENSITIVITY}. Although it is an optional tag, it should be recorded
	 *  when {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} is recorded.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 34864</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #SENSITIVITY_TYPE_UNKNOWN
	 *  @see #SENSITIVITY_TYPE_SOS
	 *  @see #SENSITIVITY_TYPE_REI
	 *  @see #SENSITIVITY_TYPE_ISO_SPEED
	 *  @see #SENSITIVITY_TYPE_SOS_AND_REI
	 *  @see #SENSITIVITY_TYPE_SOS_AND_ISO
	 *  @see #SENSITIVITY_TYPE_REI_AND_ISO
	 *  @see #SENSITIVITY_TYPE_SOS_AND_REI_AND_ISO
	 */
	public static final String TAG_SENSITIVITY_TYPE = "SensitivityType";
	/**
	 *  <p>This tag indicates the standard output sensitivity value of a camera or input device
	 *  defined in ISO 12232. When recording this tag, {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} and
	 *  {@link #TAG_SENSITIVITY_TYPE} shall also be recorded.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 34865</li>
	 *      <li>Type = Unsigned long</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_STANDARD_OUTPUT_SENSITIVITY = "StandardOutputSensitivity";
	/**
	 *  <p>This tag indicates the recommended exposure index value of a camera or input device
	 *  defined in ISO 12232. When recording this tag, {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} and
	 *  {@link #TAG_SENSITIVITY_TYPE} shall also be recorded.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 34866</li>
	 *      <li>Type = Unsigned long</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_RECOMMENDED_EXPOSURE_INDEX = "RecommendedExposureIndex";
	/**
	 *  <p>This tag indicates the ISO speed value of a camera or input device that is defined in
	 *  ISO 12232. When recording this tag, {@link #TAG_PHOTOGRAPHIC_SENSITIVITY} and
	 *  {@link #TAG_SENSITIVITY_TYPE} shall also be recorded.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 34867</li>
	 *      <li>Type = Unsigned long</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_ISO_SPEED = "ISOSpeed";
	/**
	 *  <p>This tag indicates the ISO speed latitude yyy value of a camera or input device that is
	 *  defined in ISO 12232. However, this tag shall not be recorded without {@link #TAG_ISO_SPEED}
	 *  and {@link #TAG_ISO_SPEED_LATITUDE_ZZZ}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 34868</li>
	 *      <li>Type = Unsigned long</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_ISO_SPEED_LATITUDE_YYY = "ISOSpeedLatitudeyyy";
	/**
	 *  <p>This tag indicates the ISO speed latitude zzz value of a camera or input device that is
	 *  defined in ISO 12232. However, this tag shall not be recorded without {@link #TAG_ISO_SPEED}
	 *  and {@link #TAG_ISO_SPEED_LATITUDE_YYY}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 34869</li>
	 *      <li>Type = Unsigned long</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_ISO_SPEED_LATITUDE_ZZZ = "ISOSpeedLatitudezzz";
	/**
	 *  <p>Shutter speed. The unit is the APEX setting.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37377</li>
	 *      <li>Type = Signed rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_SHUTTER_SPEED_VALUE = "ShutterSpeedValue";
	/**
	 *  <p>The lens aperture. The unit is the APEX value.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37378</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_APERTURE_VALUE = "ApertureValue";
	/**
	 *  <p>The value of brightness. The unit is the APEX value. Ordinarily it is given in the range
	 *  of -99.99 to 99.99. Note that if the numerator of the recorded value is 0xFFFFFFFF,
	 *  Unknown shall be indicated.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37379</li>
	 *      <li>Type = Signed rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_BRIGHTNESS_VALUE = "BrightnessValue";
	/**
	 *  <p>The exposure bias. The unit is the APEX value. Ordinarily it is given in the range of
	 *  -99.99 to 99.99.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37380</li>
	 *      <li>Type = Signed rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_EXPOSURE_BIAS_VALUE = "ExposureBiasValue";
	/**
	 *  <p>The smallest F number of the lens. The unit is the APEX value. Ordinarily it is given
	 *  in the range of 00.00 to 99.99, but it is not limited to this range.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37381</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_MAX_APERTURE_VALUE = "MaxApertureValue";
	/**
	 *  <p>The distance to the subject, given in meters. Note that if the numerator of the recorded
	 *  value is 0xFFFFFFFF, Infinity shall be indicated; and if the numerator is 0, Distance
	 *  unknown shall be indicated.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37382</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_SUBJECT_DISTANCE = "SubjectDistance";
	/**
	 *  <p>The metering mode.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37383</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = {@link #METERING_MODE_UNKNOWN}</li>
	 *  </ul>
	 *
	 *  @see #METERING_MODE_UNKNOWN
	 *  @see #METERING_MODE_AVERAGE
	 *  @see #METERING_MODE_CENTER_WEIGHT_AVERAGE
	 *  @see #METERING_MODE_SPOT
	 *  @see #METERING_MODE_MULTI_SPOT
	 *  @see #METERING_MODE_PATTERN
	 *  @see #METERING_MODE_PARTIAL
	 *  @see #METERING_MODE_OTHER
	 */
	public static final String TAG_METERING_MODE = "MeteringMode";
	/**
	 *  <p>The kind of light source.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37384</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = {@link #LIGHT_SOURCE_UNKNOWN}</li>
	 *  </ul>
	 *
	 *  @see #LIGHT_SOURCE_UNKNOWN
	 *  @see #LIGHT_SOURCE_DAYLIGHT
	 *  @see #LIGHT_SOURCE_FLUORESCENT
	 *  @see #LIGHT_SOURCE_TUNGSTEN
	 *  @see #LIGHT_SOURCE_FLASH
	 *  @see #LIGHT_SOURCE_FINE_WEATHER
	 *  @see #LIGHT_SOURCE_CLOUDY_WEATHER
	 *  @see #LIGHT_SOURCE_SHADE
	 *  @see #LIGHT_SOURCE_DAYLIGHT_FLUORESCENT
	 *  @see #LIGHT_SOURCE_DAY_WHITE_FLUORESCENT
	 *  @see #LIGHT_SOURCE_COOL_WHITE_FLUORESCENT
	 *  @see #LIGHT_SOURCE_WHITE_FLUORESCENT
	 *  @see #LIGHT_SOURCE_WARM_WHITE_FLUORESCENT
	 *  @see #LIGHT_SOURCE_STANDARD_LIGHT_A
	 *  @see #LIGHT_SOURCE_STANDARD_LIGHT_B
	 *  @see #LIGHT_SOURCE_STANDARD_LIGHT_C
	 *  @see #LIGHT_SOURCE_D55
	 *  @see #LIGHT_SOURCE_D65
	 *  @see #LIGHT_SOURCE_D75
	 *  @see #LIGHT_SOURCE_D50
	 *  @see #LIGHT_SOURCE_ISO_STUDIO_TUNGSTEN
	 *  @see #LIGHT_SOURCE_OTHER
	 */
	public static final String TAG_LIGHT_SOURCE = "LightSource";
	/**
	 *  <p>This tag indicates the status of flash when the image was shot. Bit 0 indicates the flash
	 *  firing status, bits 1 and 2 indicate the flash return status, bits 3 and 4 indicate
	 *  the flash mode, bit 5 indicates whether the flash function is present, and bit 6 indicates
	 *  "red eye" mode.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37385</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *  </ul>
	 *
	 *  @see #FLAG_FLASH_FIRED
	 *  @see #FLAG_FLASH_RETURN_LIGHT_NOT_DETECTED
	 *  @see #FLAG_FLASH_RETURN_LIGHT_DETECTED
	 *  @see #FLAG_FLASH_MODE_COMPULSORY_FIRING
	 *  @see #FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION
	 *  @see #FLAG_FLASH_MODE_AUTO
	 *  @see #FLAG_FLASH_NO_FLASH_FUNCTION
	 *  @see #FLAG_FLASH_RED_EYE_SUPPORTED
	 */
	public static final String TAG_FLASH = "Flash";
	/**
	 *  <p>This tag indicates the location and area of the main subject in the overall scene.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37396</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 2 or 3 or 4</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  <p>The subject location and area are defined by Count values as follows.</p>
	 *
	 *  <ul>
	 *      <li>Count = 2 Indicates the location of the main subject as coordinates. The first value
	 *                    is the X coordinate and the second is the Y coordinate.</li>
	 *      <li>Count = 3 The area of the main subject is given as a circle. The circular area is
	 *                    expressed as center coordinates and diameter. The first value is
	 *                    the center X coordinate, the second is the center Y coordinate, and
	 *                    the third is the diameter.</li>
	 *      <li>Count = 4 The area of the main subject is given as a rectangle. The rectangular
	 *                    area is expressed as center coordinates and area dimensions. The first
	 *                    value is the center X coordinate, the second is the center Y coordinate,
	 *                    the third is the width of the area, and the fourth is the height of
	 *                    the area.</li>
	 *  </ul>
	 *
	 *  <p>Note that the coordinate values, width, and height are expressed in relation to the upper
	 *  left as origin, prior to rotation processing as per {@link #TAG_ORIENTATION}.</p>
	 */
	public static final String TAG_SUBJECT_AREA = "SubjectArea";
	/**
	 *  <p>The actual focal length of the lens, in mm. Conversion is not made to the focal length
	 *  of a 35mm film camera.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 37386</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_FOCAL_LENGTH = "FocalLength";
	/**
	 *  <p>Indicates the strobe energy at the time the image is captured, as measured in Beam Candle
	 *  Power Seconds (BCPS).</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41483</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_FLASH_ENERGY = "FlashEnergy";
	/**
	 *  <p>This tag records the camera or input device spatial frequency table and SFR values in
	 *  the direction of image width, image height, and diagonal direction, as specified in
	 *  ISO 12233.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41484</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_SPATIAL_FREQUENCY_RESPONSE = "SpatialFrequencyResponse";
	/**
	 *  <p>Indicates the number of pixels in the image width (X) direction per
	 *  {@link #TAG_FOCAL_PLANE_RESOLUTION_UNIT} on the camera focal plane.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41486</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_FOCAL_PLANE_X_RESOLUTION = "FocalPlaneXResolution";
	/**
	 *  <p>Indicates the number of pixels in the image height (Y) direction per
	 *  {@link #TAG_FOCAL_PLANE_RESOLUTION_UNIT} on the camera focal plane.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41487</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_FOCAL_PLANE_Y_RESOLUTION = "FocalPlaneYResolution";
	/**
	 *  <p>Indicates the unit for measuring {@link #TAG_FOCAL_PLANE_X_RESOLUTION} and
	 *  {@link #TAG_FOCAL_PLANE_Y_RESOLUTION}. This value is the same as
	 *  {@link #TAG_RESOLUTION_UNIT}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41488</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = {@link #RESOLUTION_UNIT_INCHES}</li>
	 *  </ul>
	 *
	 *  @see #TAG_RESOLUTION_UNIT
	 *  @see #RESOLUTION_UNIT_INCHES
	 *  @see #RESOLUTION_UNIT_CENTIMETERS
	 */
	public static final String TAG_FOCAL_PLANE_RESOLUTION_UNIT = "FocalPlaneResolutionUnit";
	/**
	 *  <p>Indicates the location of the main subject in the scene. The value of this tag represents
	 *  the pixel at the center of the main subject relative to the left edge, prior to rotation
	 *  processing as per {@link #TAG_ORIENTATION}. The first value indicates the X column number
	 *  and second indicates the Y row number. When a camera records the main subject location,
	 *  it is recommended that {@link #TAG_SUBJECT_AREA} be used instead of this tag.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41492</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 2</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_SUBJECT_LOCATION = "SubjectLocation";
	/**
	 *  <p>Indicates the exposure index selected on the camera or input device at the time the image
	 *  is captured.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41493</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_EXPOSURE_INDEX = "ExposureIndex";
	/**
	 *  <p>Indicates the image sensor type on the camera or input device.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41495</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #SENSOR_TYPE_NOT_DEFINED
	 *  @see #SENSOR_TYPE_ONE_CHIP
	 *  @see #SENSOR_TYPE_TWO_CHIP
	 *  @see #SENSOR_TYPE_THREE_CHIP
	 *  @see #SENSOR_TYPE_COLOR_SEQUENTIAL
	 *  @see #SENSOR_TYPE_TRILINEAR
	 *  @see #SENSOR_TYPE_COLOR_SEQUENTIAL_LINEAR
	 */
	public static final String TAG_SENSING_METHOD = "SensingMethod";
	/**
	 *  <p>Indicates the image source. If a DSC recorded the image, this tag value always shall
	 *  be set to {@link #FILE_SOURCE_DSC}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41728</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = {@link #FILE_SOURCE_DSC}</li>
	 *  </ul>
	 *
	 *  @see #FILE_SOURCE_OTHER
	 *  @see #FILE_SOURCE_TRANSPARENT_SCANNER
	 *  @see #FILE_SOURCE_REFLEX_SCANNER
	 *  @see #FILE_SOURCE_DSC
	 */
	public static final String TAG_FILE_SOURCE = "FileSource";
	/**
	 *  <p>Indicates the type of scene. If a DSC recorded the image, this tag value shall always
	 *  be set to {@link #SCENE_TYPE_DIRECTLY_PHOTOGRAPHED}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41729</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = 1</li>
	 *  </ul>
	 *
	 *  @see #SCENE_TYPE_DIRECTLY_PHOTOGRAPHED
	 */
	public static final String TAG_SCENE_TYPE = "SceneType";
	/**
	 *  <p>Indicates the color filter array (CFA) geometric pattern of the image sensor when
	 *  a one-chip color area sensor is used. It does not apply to all sensing methods.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41730</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #TAG_SENSING_METHOD
	 *  @see #SENSOR_TYPE_ONE_CHIP
	 */
	public static final String TAG_CFA_PATTERN = "CFAPattern";
	/**
	 *  <p>This tag indicates the use of special processing on image data, such as rendering geared
	 *  to output. When special processing is performed, the Exif/DCF reader is expected to disable
	 *  or minimize any further processing.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41985</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = {@link #RENDERED_PROCESS_NORMAL}</li>
	 *  </ul>
	 *
	 *  @see #RENDERED_PROCESS_NORMAL
	 *  @see #RENDERED_PROCESS_CUSTOM
	 */
	public static final String TAG_CUSTOM_RENDERED = "CustomRendered";
	/**
	 *  <p>This tag indicates the exposure mode set when the image was shot.
	 *  In {@link #EXPOSURE_MODE_AUTO_BRACKET}, the camera shoots a series of frames of the same
	 *  scene at different exposure settings.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41986</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #EXPOSURE_MODE_AUTO
	 *  @see #EXPOSURE_MODE_MANUAL
	 *  @see #EXPOSURE_MODE_AUTO_BRACKET
	 */
	public static final String TAG_EXPOSURE_MODE = "ExposureMode";
	/**
	 *  <p>This tag indicates the white balance mode set when the image was shot.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41987</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #WHITEBALANCE_AUTO
	 *  @see #WHITEBALANCE_MANUAL
	 */
	public static final String TAG_WHITE_BALANCE = "WhiteBalance";
	/**
	 *  <p>This tag indicates the digital zoom ratio when the image was shot. If the numerator of
	 *  the recorded value is 0, this indicates that digital zoom was not used.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41988</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_DIGITAL_ZOOM_RATIO = "DigitalZoomRatio";
	/**
	 *  <p>This tag indicates the equivalent focal length assuming a 35mm film camera, in mm.
	 *  A value of 0 means the focal length is unknown. Note that this tag differs from
	 *  {@link #TAG_FOCAL_LENGTH}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41989</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_FOCAL_LENGTH_IN_35MM_FILM = "FocalLengthIn35mmFilm";
	/**
	 *  <p>This tag indicates the type of scene that was shot. It may also be used to record
	 *  the mode in which the image was shot. Note that this differs from
	 *  {@link #TAG_SCENE_TYPE}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41990</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = 0</li>
	 *  </ul>
	 *
	 *  @see #SCENE_CAPTURE_TYPE_STANDARD
	 *  @see #SCENE_CAPTURE_TYPE_LANDSCAPE
	 *  @see #SCENE_CAPTURE_TYPE_PORTRAIT
	 *  @see #SCENE_CAPTURE_TYPE_NIGHT
	 */
	public static final String TAG_SCENE_CAPTURE_TYPE = "SceneCaptureType";
	/**
	 *  <p>This tag indicates the degree of overall image gain adjustment.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41991</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #GAIN_CONTROL_NONE
	 *  @see #GAIN_CONTROL_LOW_GAIN_UP
	 *  @see #GAIN_CONTROL_HIGH_GAIN_UP
	 *  @see #GAIN_CONTROL_LOW_GAIN_DOWN
	 *  @see #GAIN_CONTROL_HIGH_GAIN_DOWN
	 */
	public static final String TAG_GAIN_CONTROL = "GainControl";
	/**
	 *  <p>This tag indicates the direction of contrast processing applied by the camera when
	 *  the image was shot.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41992</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = {@link #CONTRAST_NORMAL}</li>
	 *  </ul>
	 *
	 *  @see #CONTRAST_NORMAL
	 *  @see #CONTRAST_SOFT
	 *  @see #CONTRAST_HARD
	 */
	public static final String TAG_CONTRAST = "Contrast";
	/**
	 *  <p>This tag indicates the direction of saturation processing applied by the camera when
	 *  the image was shot.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41993</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = {@link #SATURATION_NORMAL}</li>
	 *  </ul>
	 *
	 *  @see #SATURATION_NORMAL
	 *  @see #SATURATION_LOW
	 *  @see #SATURATION_HIGH
	 */
	public static final String TAG_SATURATION = "Saturation";
	/**
	 *  <p>This tag indicates the direction of sharpness processing applied by the camera when
	 *  the image was shot.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41994</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = {@link #SHARPNESS_NORMAL}</li>
	 *  </ul>
	 *
	 *  @see #SHARPNESS_NORMAL
	 *  @see #SHARPNESS_SOFT
	 *  @see #SHARPNESS_HARD
	 */
	public static final String TAG_SHARPNESS = "Sharpness";
	/**
	 *  <p>This tag indicates information on the picture-taking conditions of a particular camera
	 *  model. The tag is used only to indicate the picture-taking conditions in the Exif/DCF
	 *  reader.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41995</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_DEVICE_SETTING_DESCRIPTION = "DeviceSettingDescription";
	/**
	 *  <p>This tag indicates the distance to the subject.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 41996</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #SUBJECT_DISTANCE_RANGE_UNKNOWN
	 *  @see #SUBJECT_DISTANCE_RANGE_MACRO
	 *  @see #SUBJECT_DISTANCE_RANGE_CLOSE_VIEW
	 *  @see #SUBJECT_DISTANCE_RANGE_DISTANT_VIEW
	 */
	public static final String TAG_SUBJECT_DISTANCE_RANGE = "SubjectDistanceRange";

	// H. Other tags
	/**
	 *  <p>This tag indicates an identifier assigned uniquely to each image. It is recorded as
	 *  an ASCII string equivalent to hexadecimal notation and 128-bit fixed length.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 42016</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 32</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_IMAGE_UNIQUE_ID = "ImageUniqueID";
	/**
	 *  <p>This tag records the owner of a camera used in photography as an ASCII string.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 42032</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_CAMARA_OWNER_NAME = "CameraOwnerName";
	/**
	 *  <p>This tag records the serial number of the body of the camera that was used in photography
	 *  as an ASCII string.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 42033</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_BODY_SERIAL_NUMBER = "BodySerialNumber";
	/**
	 *  <p>This tag notes minimum focal length, maximum focal length, minimum F number in the
	 *  minimum focal length, and minimum F number in the maximum focal length, which are
	 *  specification information for the lens that was used in photography. When the minimum
	 *  F number is unknown, the notation is 0/0.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 42034</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 4</li>
	 *      <li>Default = None</li>
	 *      <ul>
	 *          <li>Value 1 := Minimum focal length (unit: mm)</li>
	 *          <li>Value 2 : = Maximum focal length (unit: mm)</li>
	 *          <li>Value 3 : = Minimum F number in the minimum focal length</li>
	 *          <li>Value 4 : = Minimum F number in the maximum focal length</li>
	 *      </ul>
	 *  </ul>
	 */
	public static final String TAG_LENS_SPECIFICATION = "LensSpecification";
	/**
	 *  <p>This tag records the lens manufacturer as an ASCII string.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 42035</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_LENS_MAKE = "LensMake";
	/**
	 *  <p>This tag records the lens’s model name and model number as an ASCII string.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 42036</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_LENS_MODEL = "LensModel";
	/**
	 *  <p>This tag records the serial number of the interchangeable lens that was used in
	 *  photography as an ASCII string.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 42037</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_LENS_SERIAL_NUMBER = "LensSerialNumber";

	// GPS Attribute Information
	/**
	 *  <p>Indicates the version of GPS Info IFD. The version is given as 2.3.0.0. This tag is
	 *  mandatory when GPS-related tags are present. Note that this tag is written as a different
	 *  byte than {@link #TAG_EXIF_VERSION}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 0</li>
	 *      <li>Type = Byte</li>
	 *      <li>Count = 4</li>
	 *      <li>Default = 2.3.0.0</li>
	 *      <ul>
	 *          <li>2300 = Version 2.3</li>
	 *          <li>Other = reserved</li>
	 *      </ul>
	 *  </ul>
	 */
	public static final String TAG_GPS_VERSION_ID = "GPSVersionID";
	/**
	 *  <p>Indicates whether the latitude is north or south latitude.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 1</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #LATITUDE_NORTH
	 *  @see #LATITUDE_SOUTH
	 */
	public static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef";
	/**
	 *  <p>Indicates the latitude. The latitude is expressed as three RATIONAL values giving
	 *  the degrees, minutes, and seconds, respectively. If latitude is expressed as degrees,
	 *  minutes and seconds, a typical format would be dd/1,mm/1,ss/1. When degrees and minutes are
	 *  used and, for example, fractions of minutes are given up to two decimal places, the format
	 *  would be dd/1,mmmm/100,0/1.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 2</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 3</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_LATITUDE = "GPSLatitude";
	/**
	 *  <p>Indicates whether the longitude is east or west longitude.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 3</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #LONGITUDE_EAST
	 *  @see #LONGITUDE_WEST
	 */
	public static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef";
	/**
	 *  <p>Indicates the longitude. The longitude is expressed as three RATIONAL values giving
	 *  the degrees, minutes, and seconds, respectively. If longitude is expressed as degrees,
	 *  minutes and seconds, a typical format would be ddd/1,mm/1,ss/1. When degrees and minutes
	 *  are used and, for example, fractions of minutes are given up to two decimal places,
	 *  the format would be ddd/1,mmmm/100,0/1.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 4</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 3</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_LONGITUDE = "GPSLongitude";
	/**
	 *  <p>Indicates the altitude used as the reference altitude. If the reference is sea level
	 *  and the altitude is above sea level, 0 is given. If the altitude is below sea level,
	 *  a value of 1 is given and the altitude is indicated as an absolute value in
	 *  {@link #TAG_GPS_ALTITUDE}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 5</li>
	 *      <li>Type = Byte</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = 0</li>
	 *  </ul>
	 *
	 *  @see #ALTITUDE_ABOVE_SEA_LEVEL
	 *  @see #ALTITUDE_BELOW_SEA_LEVEL
	 */
	public static final String TAG_GPS_ALTITUDE_REF = "GPSAltitudeRef";
	/**
	 *  <p>Indicates the altitude based on the reference in {@link #TAG_GPS_ALTITUDE_REF}.
	 *  The reference unit is meters.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 6</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_ALTITUDE = "GPSAltitude";
	/**
	 *  <p>Indicates the time as UTC (Coordinated Universal Time). TimeStamp is expressed as three
	 *  unsigned rational values giving the hour, minute, and second.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 7</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 3</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_TIMESTAMP = "GPSTimeStamp";
	/**
	 *  <p>Indicates the GPS satellites used for measurements. This tag may be used to describe
	 *  the number of satellites, their ID number, angle of elevation, azimuth, SNR and other
	 *  information in ASCII notation. The format is not specified. If the GPS receiver is incapable
	 *  of taking measurements, value of the tag shall be set to {@code null}.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 8</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_SATELLITES = "GPSSatellites";
	/**
	 *  <p>Indicates the status of the GPS receiver when the image is recorded. 'A' means
	 *  measurement is in progress, and 'V' means the measurement is interrupted.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 9</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #GPS_MEASUREMENT_IN_PROGRESS
	 *  @see #GPS_MEASUREMENT_INTERRUPTED
	 */
	public static final String TAG_GPS_STATUS = "GPSStatus";
	/**
	 *  <p>Indicates the GPS measurement mode. Originally it was defined for GPS, but it may
	 *  be used for recording a measure mode to record the position information provided from
	 *  a mobile base station or wireless LAN as well as GPS.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 10</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #GPS_MEASUREMENT_2D
	 *  @see #GPS_MEASUREMENT_3D
	 */
	public static final String TAG_GPS_MEASURE_MODE = "GPSMeasureMode";
	/**
	 *  <p>Indicates the GPS DOP (data degree of precision). An HDOP value is written during
	 *  two-dimensional measurement, and PDOP during three-dimensional measurement.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 11</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_DOP = "GPSDOP";
	/**
	 *  <p>Indicates the unit used to express the GPS receiver speed of movement.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 12</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = {@link #GPS_SPEED_KILOMETERS_PER_HOUR}</li>
	 *  </ul>
	 *
	 *  @see #GPS_SPEED_KILOMETERS_PER_HOUR
	 *  @see #GPS_SPEED_MILES_PER_HOUR
	 *  @see #GPS_SPEED_KNOTS
	 */
	public static final String TAG_GPS_SPEED_REF = "GPSSpeedRef";
	/**
	 *  <p>Indicates the speed of GPS receiver movement.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 13</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_SPEED = "GPSSpeed";
	/**
	 *  <p>Indicates the reference for giving the direction of GPS receiver movement.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 14</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
	 *  </ul>
	 *
	 *  @see #GPS_DIRECTION_TRUE
	 *  @see #GPS_DIRECTION_MAGNETIC
	 */
	public static final String TAG_GPS_TRACK_REF = "GPSTrackRef";
	/**
	 *  <p>Indicates the direction of GPS receiver movement.
	 *  The range of values is from 0.00 to 359.99.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 15</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_TRACK = "GPSTrack";
	/**
	 *  <p>Indicates the reference for giving the direction of the image when it is captured.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 16</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
	 *  </ul>
	 *
	 *  @see #GPS_DIRECTION_TRUE
	 *  @see #GPS_DIRECTION_MAGNETIC
	 */
	public static final String TAG_GPS_IMG_DIRECTION_REF = "GPSImgDirectionRef";
	/**
	 *  <p>ndicates the direction of the image when it was captured.
	 *  The range of values is from 0.00 to 359.99.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 17</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_IMG_DIRECTION = "GPSImgDirection";
	/**
	 *  <p>Indicates the geodetic survey data used by the GPS receiver. If the survey data is
	 *  restricted to Japan,the value of this tag is 'TOKYO' or 'WGS-84'. If a GPS Info tag is
	 *  recorded, it is strongly recommended that this tag be recorded.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 18</li>
	 *      <li>Type = String</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_MAP_DATUM = "GPSMapDatum";
	/**
	 *  <p>Indicates whether the latitude of the destination point is north or south latitude.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 19</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #LATITUDE_NORTH
	 *  @see #LATITUDE_SOUTH
	 */
	public static final String TAG_GPS_DEST_LATITUDE_REF = "GPSDestLatitudeRef";
	/**
	 *  <p>Indicates the latitude of the destination point. The latitude is expressed as three
	 *  unsigned rational values giving the degrees, minutes, and seconds, respectively.
	 *  If latitude is expressed as degrees, minutes and seconds, a typical format would be
	 *  dd/1,mm/1,ss/1. When degrees and minutes are used and, for example, fractions of minutes
	 *  are given up to two decimal places, the format would be dd/1, mmmm/100, 0/1.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 20</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 3</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_DEST_LATITUDE = "GPSDestLatitude";
	/**
	 *  <p>Indicates whether the longitude of the destination point is east or west longitude.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 21</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #LONGITUDE_EAST
	 *  @see #LONGITUDE_WEST
	 */
	public static final String TAG_GPS_DEST_LONGITUDE_REF = "GPSDestLongitudeRef";
	/**
	 *  <p>Indicates the longitude of the destination point. The longitude is expressed as three
	 *  unsigned rational values giving the degrees, minutes, and seconds, respectively.
	 *  If longitude is expressed as degrees, minutes and seconds, a typical format would be ddd/1,
	 *  mm/1, ss/1. When degrees and minutes are used and, for example, fractions of minutes are
	 *  given up to two decimal places, the format would be ddd/1, mmmm/100, 0/1.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 22</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 3</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_DEST_LONGITUDE = "GPSDestLongitude";
	/**
	 *  <p>Indicates the reference used for giving the bearing to the destination point.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 23</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = {@link #GPS_DIRECTION_TRUE}</li>
	 *  </ul>
	 *
	 *  @see #GPS_DIRECTION_TRUE
	 *  @see #GPS_DIRECTION_MAGNETIC
	 */
	public static final String TAG_GPS_DEST_BEARING_REF = "GPSDestBearingRef";
	/**
	 *  <p>Indicates the bearing to the destination point.
	 *  The range of values is from 0.00 to 359.99.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 24</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_DEST_BEARING = "GPSDestBearing";
	/**
	 *  <p>Indicates the unit used to express the distance to the destination point.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 25</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 1</li>
	 *      <li>Default = {@link #GPS_DISTANCE_KILOMETERS}</li>
	 *  </ul>
	 *
	 *  @see #GPS_DISTANCE_KILOMETERS
	 *  @see #GPS_DISTANCE_MILES
	 *  @see #GPS_DISTANCE_NAUTICAL_MILES
	 */
	public static final String TAG_GPS_DEST_DISTANCE_REF = "GPSDestDistanceRef";
	/**
	 *  <p>Indicates the distance to the destination point.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 26</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_DEST_DISTANCE = "GPSDestDistance";
	/**
	 *  <p>A character string recording the name of the method used for location finding.
	 *  The first byte indicates the character code used, and this is followed by the name of
	 *  the method.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 27</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod";
	/**
	 *  <p>A character string recording the name of the GPS area. The first byte indicates
	 *  the character code used, and this is followed by the name of the GPS area.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 28</li>
	 *      <li>Type = Undefined</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_AREA_INFORMATION = "GPSAreaInformation";
	/**
	 *  <p>A character string recording date and time information relative to UTC (Coordinated
	 *  Universal Time). The format is "YYYY:MM:DD".</p>
	 *
	 *  <ul>
	 *      <li>Tag = 29</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 10</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_DATESTAMP = "GPSDateStamp";
	/**
	 *  <p>Indicates whether differential correction is applied to the GPS receiver.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 30</li>
	 *      <li>Type = Unsigned short</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 *
	 *  @see #GPS_MEASUREMENT_NO_DIFFERENTIAL
	 *  @see #GPS_MEASUREMENT_DIFFERENTIAL_CORRECTED
	 */
	public static final String TAG_GPS_DIFFERENTIAL = "GPSDifferential";
	/**
	 *  <p>This tag indicates horizontal positioning errors in meters.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 31</li>
	 *      <li>Type = Unsigned rational</li>
	 *      <li>Count = 1</li>
	 *      <li>Default = None</li>
	 *  </ul>
	 */
	public static final String TAG_GPS_H_POSITIONING_ERROR = "GPSHPositioningError";

	// Interoperability IFD Attribute Information
	/**
	 *  <p>Indicates the identification of the Interoperability rule.</p>
	 *
	 *  <ul>
	 *      <li>Tag = 1</li>
	 *      <li>Type = String</li>
	 *      <li>Length = 4</li>
	 *      <li>Default = None</li>
	 *      <ul>
	 *          <li>"R98" = Indicates a file conforming to R98 file specification of Recommended
	 *                      Exif Interoperability Rules (Exif R 98) or to DCF basic file stipulated
	 *                      by Design Rule for Camera File System.</li>
	 *          <li>"THM" = Indicates a file conforming to DCF thumbnail file stipulated by Design
	 *                      rule for Camera File System.</li>
	 *          <li>“R03” = Indicates a file conforming to DCF Option File stipulated by Design rule
	 *                      for Camera File System.</li>
	 *      </ul>
	 *  </ul>
	 */
	public static final String TAG_INTEROPERABILITY_INDEX = "InteroperabilityIndex";

	/**
	 * @see #TAG_IMAGE_LENGTH
	 */
	public static final String TAG_THUMBNAIL_IMAGE_LENGTH = "ThumbnailImageLength";
	/**
	 * @see #TAG_IMAGE_WIDTH
	 */
	public static final String TAG_THUMBNAIL_IMAGE_WIDTH = "ThumbnailImageWidth";
	/** Type is int. DNG Specification 1.4.0.0. Section 4 */
	public static final String TAG_DNG_VERSION = "DNGVersion";
	/** Type is int. DNG Specification 1.4.0.0. Section 4 */
	public static final String TAG_DEFAULT_CROP_SIZE = "DefaultCropSize";
	/** Type is undefined. See Olympus MakerNote tags in http://www.exiv2.org/tags-olympus.html. */
	public static final String TAG_ORF_THUMBNAIL_IMAGE = "ThumbnailImage";
	/** Type is int. See Olympus Camera Settings tags in http://www.exiv2.org/tags-olympus.html. */
	public static final String TAG_ORF_PREVIEW_IMAGE_START = "PreviewImageStart";
	/** Type is int. See Olympus Camera Settings tags in http://www.exiv2.org/tags-olympus.html. */
	public static final String TAG_ORF_PREVIEW_IMAGE_LENGTH = "PreviewImageLength";
	/** Type is int. See Olympus Image Processing tags in http://www.exiv2.org/tags-olympus.html. */
	public static final String TAG_ORF_ASPECT_FRAME = "AspectFrame";
	/**
	 * Type is int. See PanasonicRaw tags in
	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
	 */
	public static final String TAG_RW2_SENSOR_BOTTOM_BORDER = "SensorBottomBorder";
	/**
	 * Type is int. See PanasonicRaw tags in
	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
	 */
	public static final String TAG_RW2_SENSOR_LEFT_BORDER = "SensorLeftBorder";
	/**
	 * Type is int. See PanasonicRaw tags in
	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
	 */
	public static final String TAG_RW2_SENSOR_RIGHT_BORDER = "SensorRightBorder";
	/**
	 * Type is int. See PanasonicRaw tags in
	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
	 */
	public static final String TAG_RW2_SENSOR_TOP_BORDER = "SensorTopBorder";
	/**
	 * Type is int. See PanasonicRaw tags in
	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
	 */
	public static final String TAG_RW2_ISO = "ISO";
	/**
	 * Type is undefined. See PanasonicRaw tags in
	 * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html
	 */
	public static final String TAG_RW2_JPG_FROM_RAW = "JpgFromRaw";
	/** Type is int. See JEITA CP-3451C Spec Section 3: Bilevel Images. */
	public static final String TAG_NEW_SUBFILE_TYPE = "NewSubfileType";
	/** Type is int. See JEITA CP-3451C Spec Section 3: Bilevel Images. */
	public static final String TAG_SUBFILE_TYPE = "SubfileType";

	/**
	 * Private tags used for pointing the other IFD offsets.
	 * The types of the following tags are int.
	 * See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
	 * For SubIFD, see Note 1 of Adobe PageMaker® 6.0 TIFF Technical Notes.
	 */
	private static final String TAG_EXIF_IFD_POINTER = "ExifIFDPointer";
	private static final String TAG_GPS_INFO_IFD_POINTER = "GPSInfoIFDPointer";
	private static final String TAG_INTEROPERABILITY_IFD_POINTER = "InteroperabilityIFDPointer";
	private static final String TAG_SUB_IFD_POINTER = "SubIFDPointer";
	// Proprietary pointer tags used for ORF files.
	// See http://www.exiv2.org/tags-olympus.html
	private static final String TAG_ORF_CAMERA_SETTINGS_IFD_POINTER = "CameraSettingsIFDPointer";
	private static final String TAG_ORF_IMAGE_PROCESSING_IFD_POINTER = "ImageProcessingIFDPointer";

	// Private tags used for thumbnail information.
	private static final String TAG_HAS_THUMBNAIL = "HasThumbnail";
	private static final String TAG_THUMBNAIL_OFFSET = "ThumbnailOffset";
	private static final String TAG_THUMBNAIL_LENGTH = "ThumbnailLength";
	private static final String TAG_THUMBNAIL_DATA = "ThumbnailData";
	private static final int MAX_THUMBNAIL_SIZE = 512;

	// Constants used for the Orientation Exif tag.
	public static final int ORIENTATION_UNDEFINED = 0;
	public static final int ORIENTATION_NORMAL = 1;
	/**
	 * Indicates the image is left right reversed mirror.
	 */
	public static final int ORIENTATION_FLIP_HORIZONTAL = 2;
	/**
	 * Indicates the image is rotated by 180 degree clockwise.
	 */
	public static final int ORIENTATION_ROTATE_180 = 3;
	/**
	 * Indicates the image is upside down mirror, it can also be represented by flip
	 * horizontally firstly and rotate 180 degree clockwise.
	 */
	public static final int ORIENTATION_FLIP_VERTICAL = 4;
	/**
	 * Indicates the image is flipped about top-left <--> bottom-right axis, it can also be
	 * represented by flip horizontally firstly and rotate 270 degree clockwise.
	 */
	public static final int ORIENTATION_TRANSPOSE = 5;
	/**
	 * Indicates the image is rotated by 90 degree clockwise.
	 */
	public static final int ORIENTATION_ROTATE_90 = 6;
	/**
	 * Indicates the image is flipped about top-right <--> bottom-left axis, it can also be
	 * represented by flip horizontally firstly and rotate 90 degree clockwise.
	 */
	public static final int ORIENTATION_TRANSVERSE = 7;
	/**
	 * Indicates the image is rotated by 270 degree clockwise.
	 */
	public static final int ORIENTATION_ROTATE_270 = 8;
	private static final List<Integer> ROTATION_ORDER = Arrays.asList(ORIENTATION_NORMAL,
			ORIENTATION_ROTATE_90, ORIENTATION_ROTATE_180, ORIENTATION_ROTATE_270);
	private static final List<Integer> FLIPPED_ROTATION_ORDER = Arrays.asList(
			ORIENTATION_FLIP_HORIZONTAL, ORIENTATION_TRANSVERSE, ORIENTATION_FLIP_VERTICAL,
			ORIENTATION_TRANSPOSE);

	/**
	 * The contant used by {@link #TAG_PLANAR_CONFIGURATION} to denote Chunky format.
	 */
	public static final short FORMAT_CHUNKY = 1;
	/**
	 * The contant used by {@link #TAG_PLANAR_CONFIGURATION} to denote Planar format.
	 */
	public static final short FORMAT_PLANAR = 2;

	/**
	 * The contant used by {@link #TAG_Y_CB_CR_POSITIONING} to denote Centered positioning.
	 */
	public static final short Y_CB_CR_POSITIONING_CENTERED = 1;
	/**
	 * The contant used by {@link #TAG_Y_CB_CR_POSITIONING} to denote Co-sited positioning.
	 */
	public static final short Y_CB_CR_POSITIONING_CO_SITED = 2;

	/**
	 * The contant used to denote resolution unit as inches.
	 */
	public static final short RESOLUTION_UNIT_INCHES = 2;
	/**
	 * The contant used to denote resolution unit as centimeters.
	 */
	public static final short RESOLUTION_UNIT_CENTIMETERS = 3;

	/**
	 * The contant used by {@link #TAG_COLOR_SPACE} to denote sRGB color space.
	 */
	public static final int COLOR_SPACE_S_RGB = 1;
	/**
	 * The contant used by {@link #TAG_COLOR_SPACE} to denote Uncalibrated.
	 */
	public static final int COLOR_SPACE_UNCALIBRATED = 65535;

	/**
	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is not defined.
	 */
	public static final short EXPOSURE_PROGRAM_NOT_DEFINED = 0;
	/**
	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Manual.
	 */
	public static final short EXPOSURE_PROGRAM_MANUAL = 1;
	/**
	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Normal.
	 */
	public static final short EXPOSURE_PROGRAM_NORMAL = 2;
	/**
	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is
	 * Aperture priority.
	 */
	public static final short EXPOSURE_PROGRAM_APERTURE_PRIORITY = 3;
	/**
	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is
	 * Shutter priority.
	 */
	public static final short EXPOSURE_PROGRAM_SHUTTER_PRIORITY = 4;
	/**
	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Creative
	 * program (biased toward depth of field).
	 */
	public static final short EXPOSURE_PROGRAM_CREATIVE = 5;
	/**
	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Action
	 * program (biased toward fast shutter speed).
	 */
	public static final short EXPOSURE_PROGRAM_ACTION = 6;
	/**
	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Portrait mode
	 * (for closeup photos with the background out of focus).
	 */
	public static final short EXPOSURE_PROGRAM_PORTRAIT_MODE = 7;
	/**
	 * The contant used by {@link #TAG_EXPOSURE_PROGRAM} to denote exposure program is Landscape
	 * mode (for landscape photos with the background in focus).
	 */
	public static final short EXPOSURE_PROGRAM_LANDSCAPE_MODE = 8;

	/**
	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is unknown.
	 */
	public static final short SENSITIVITY_TYPE_UNKNOWN = 0;
	/**
	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
	 * output sensitivity (SOS).
	 */
	public static final short SENSITIVITY_TYPE_SOS = 1;
	/**
	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Recommended
	 * exposure index (REI).
	 */
	public static final short SENSITIVITY_TYPE_REI = 2;
	/**
	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is ISO speed.
	 */
	public static final short SENSITIVITY_TYPE_ISO_SPEED = 3;
	/**
	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
	 * output sensitivity (SOS) and recommended exposure index (REI).
	 */
	public static final short SENSITIVITY_TYPE_SOS_AND_REI = 4;
	/**
	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
	 * output sensitivity (SOS) and ISO speed.
	 */
	public static final short SENSITIVITY_TYPE_SOS_AND_ISO = 5;
	/**
	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Recommended
	 * exposure index (REI) and ISO speed.
	 */
	public static final short SENSITIVITY_TYPE_REI_AND_ISO = 6;
	/**
	 * The contant used by {@link #TAG_SENSITIVITY_TYPE} to denote sensitivity type is Standard
	 * output sensitivity (SOS) and recommended exposure index (REI) and ISO speed.
	 */
	public static final short SENSITIVITY_TYPE_SOS_AND_REI_AND_ISO = 7;

	/**
	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is unknown.
	 */
	public static final short METERING_MODE_UNKNOWN = 0;
	/**
	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is Average.
	 */
	public static final short METERING_MODE_AVERAGE = 1;
	/**
	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is
	 * CenterWeightedAverage.
	 */
	public static final short METERING_MODE_CENTER_WEIGHT_AVERAGE = 2;
	/**
	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is Spot.
	 */
	public static final short METERING_MODE_SPOT = 3;
	/**
	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is MultiSpot.
	 */
	public static final short METERING_MODE_MULTI_SPOT = 4;
	/**
	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is Pattern.
	 */
	public static final short METERING_MODE_PATTERN = 5;
	/**
	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is Partial.
	 */
	public static final short METERING_MODE_PARTIAL = 6;
	/**
	 * The contant used by {@link #TAG_METERING_MODE} to denote metering mode is other.
	 */
	public static final short METERING_MODE_OTHER = 255;

	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is unknown.
	 */
	public static final short LIGHT_SOURCE_UNKNOWN = 0;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Daylight.
	 */
	public static final short LIGHT_SOURCE_DAYLIGHT = 1;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Fluorescent.
	 */
	public static final short LIGHT_SOURCE_FLUORESCENT = 2;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Tungsten
	 * (incandescent light).
	 */
	public static final short LIGHT_SOURCE_TUNGSTEN = 3;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Flash.
	 */
	public static final short LIGHT_SOURCE_FLASH = 4;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Fine weather.
	 */
	public static final short LIGHT_SOURCE_FINE_WEATHER = 9;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Cloudy weather.
	 */
	public static final short LIGHT_SOURCE_CLOUDY_WEATHER = 10;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Shade.
	 */
	public static final short LIGHT_SOURCE_SHADE = 11;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Daylight fluorescent
	 * (D 5700 - 7100K).
	 */
	public static final short LIGHT_SOURCE_DAYLIGHT_FLUORESCENT = 12;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Day white fluorescent
	 * (N 4600 - 5500K).
	 */
	public static final short LIGHT_SOURCE_DAY_WHITE_FLUORESCENT = 13;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Cool white
	 * fluorescent (W 3800 - 4500K).
	 */
	public static final short LIGHT_SOURCE_COOL_WHITE_FLUORESCENT = 14;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is White fluorescent
	 * (WW 3250 - 3800K).
	 */
	public static final short LIGHT_SOURCE_WHITE_FLUORESCENT = 15;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Warm white
	 * fluorescent (L 2600 - 3250K).
	 */
	public static final short LIGHT_SOURCE_WARM_WHITE_FLUORESCENT = 16;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Standard light A.
	 */
	public static final short LIGHT_SOURCE_STANDARD_LIGHT_A = 17;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Standard light B.
	 */
	public static final short LIGHT_SOURCE_STANDARD_LIGHT_B = 18;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is Standard light C.
	 */
	public static final short LIGHT_SOURCE_STANDARD_LIGHT_C = 19;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D55.
	 */
	public static final short LIGHT_SOURCE_D55 = 20;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D65.
	 */
	public static final short LIGHT_SOURCE_D65 = 21;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D75.
	 */
	public static final short LIGHT_SOURCE_D75 = 22;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is D50.
	 */
	public static final short LIGHT_SOURCE_D50 = 23;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is ISO studio tungsten.
	 */
	public static final short LIGHT_SOURCE_ISO_STUDIO_TUNGSTEN = 24;
	/**
	 * The contant used by {@link #TAG_LIGHT_SOURCE} to denote light source is other.
	 */
	public static final short LIGHT_SOURCE_OTHER = 255;

	/**
	 * The flag used by {@link #TAG_FLASH} to indicate whether the flash is fired.
	 */
	public static final short FLAG_FLASH_FIRED = 0b0000_0001;
	/**
	 * The flag used by {@link #TAG_FLASH} to indicate strobe return light is not detected.
	 */
	public static final short FLAG_FLASH_RETURN_LIGHT_NOT_DETECTED = 0b0000_0100;
	/**
	 * The flag used by {@link #TAG_FLASH} to indicate strobe return light is detected.
	 */
	public static final short FLAG_FLASH_RETURN_LIGHT_DETECTED = 0b0000_0110;
	/**
	 * The flag used by {@link #TAG_FLASH} to indicate the camera's flash mode is Compulsory flash
	 * firing.
	 *
	 * @see #FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION
	 * @see #FLAG_FLASH_MODE_AUTO
	 */
	public static final short FLAG_FLASH_MODE_COMPULSORY_FIRING = 0b0000_1000;
	/**
	 * The flag used by {@link #TAG_FLASH} to indicate the camera's flash mode is Compulsory flash
	 * suppression.
	 *
	 * @see #FLAG_FLASH_MODE_COMPULSORY_FIRING
	 * @see #FLAG_FLASH_MODE_AUTO
	 */
	public static final short FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION = 0b0001_0000;
	/**
	 * The flag used by {@link #TAG_FLASH} to indicate the camera's flash mode is Auto.
	 *
	 * @see #FLAG_FLASH_MODE_COMPULSORY_FIRING
	 * @see #FLAG_FLASH_MODE_COMPULSORY_SUPPRESSION
	 */
	public static final short FLAG_FLASH_MODE_AUTO = 0b0001_1000;
	/**
	 * The flag used by {@link #TAG_FLASH} to indicate no flash function is present.
	 */
	public static final short FLAG_FLASH_NO_FLASH_FUNCTION = 0b0010_0000;
	/**
	 * The flag used by {@link #TAG_FLASH} to indicate red-eye reduction is supported.
	 */
	public static final short FLAG_FLASH_RED_EYE_SUPPORTED = 0b0100_0000;

	/**
	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is not
	 * defined.
	 */
	public static final short SENSOR_TYPE_NOT_DEFINED = 1;
	/**
	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is One-chip
	 * color area sensor.
	 */
	public static final short SENSOR_TYPE_ONE_CHIP = 2;
	/**
	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Two-chip
	 * color area sensor.
	 */
	public static final short SENSOR_TYPE_TWO_CHIP = 3;
	/**
	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Three-chip
	 * color area sensor.
	 */
	public static final short SENSOR_TYPE_THREE_CHIP = 4;
	/**
	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Color
	 * sequential area sensor.
	 */
	public static final short SENSOR_TYPE_COLOR_SEQUENTIAL = 5;
	/**
	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Trilinear
	 * sensor.
	 */
	public static final short SENSOR_TYPE_TRILINEAR = 7;
	/**
	 * The contant used by {@link #TAG_SENSING_METHOD} to denote the image sensor type is Color
	 * sequential linear sensor.
	 */
	public static final short SENSOR_TYPE_COLOR_SEQUENTIAL_LINEAR = 8;

	/**
	 * The contant used by {@link #TAG_FILE_SOURCE} to denote the source is other.
	 */
	public static final short FILE_SOURCE_OTHER = 0;
	/**
	 * The contant used by {@link #TAG_FILE_SOURCE} to denote the source is scanner of transparent
	 * type.
	 */
	public static final short FILE_SOURCE_TRANSPARENT_SCANNER = 1;
	/**
	 * The contant used by {@link #TAG_FILE_SOURCE} to denote the source is scanner of reflex type.
	 */
	public static final short FILE_SOURCE_REFLEX_SCANNER = 2;
	/**
	 * The contant used by {@link #TAG_FILE_SOURCE} to denote the source is DSC.
	 */
	public static final short FILE_SOURCE_DSC = 3;

	/**
	 * The contant used by {@link #TAG_SCENE_TYPE} to denote the scene is directly photographed.
	 */
	public static final short SCENE_TYPE_DIRECTLY_PHOTOGRAPHED = 1;

	/**
	 * The contant used by {@link #TAG_CUSTOM_RENDERED} to denote no special processing is used.
	 */
	public static final short RENDERED_PROCESS_NORMAL = 0;
	/**
	 * The contant used by {@link #TAG_CUSTOM_RENDERED} to denote special processing is used.
	 */
	public static final short RENDERED_PROCESS_CUSTOM = 1;

	/**
	 * The contant used by {@link #TAG_EXPOSURE_MODE} to denote the exposure mode is Auto.
	 */
	public static final short EXPOSURE_MODE_AUTO = 0;
	/**
	 * The contant used by {@link #TAG_EXPOSURE_MODE} to denote the exposure mode is Manual.
	 */
	public static final short EXPOSURE_MODE_MANUAL = 1;
	/**
	 * The contant used by {@link #TAG_EXPOSURE_MODE} to denote the exposure mode is Auto bracket.
	 */
	public static final short EXPOSURE_MODE_AUTO_BRACKET = 2;

	/**
	 * The contant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Auto.
	 *
	 * @deprecated Use {@link #WHITE_BALANCE_AUTO} instead.
	 */
	@Deprecated public static final int WHITEBALANCE_AUTO = 0;
	/**
	 * The contant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Manual.
	 *
	 * @deprecated Use {@link #WHITE_BALANCE_MANUAL} instead.
	 */
	@Deprecated public static final int WHITEBALANCE_MANUAL = 1;
	/**
	 * The contant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Auto.
	 */
	public static final short WHITE_BALANCE_AUTO = 0;
	/**
	 * The contant used by {@link #TAG_WHITE_BALANCE} to denote the white balance is Manual.
	 */
	public static final short WHITE_BALANCE_MANUAL = 1;

	/**
	 * The contant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is
	 * Standard.
	 */
	public static final short SCENE_CAPTURE_TYPE_STANDARD = 0;
	/**
	 * The contant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is
	 * Landscape.
	 */
	public static final short SCENE_CAPTURE_TYPE_LANDSCAPE = 1;
	/**
	 * The contant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is
	 * Portrait.
	 */
	public static final short SCENE_CAPTURE_TYPE_PORTRAIT = 2;
	/**
	 * The contant used by {@link #TAG_SCENE_CAPTURE_TYPE} to denote the scene capture type is Night
	 * scene.
	 */
	public static final short SCENE_CAPTURE_TYPE_NIGHT = 3;

	/**
	 * The contant used by {@link #TAG_GAIN_CONTROL} to denote none gain adjustment.
	 */
	public static final short GAIN_CONTROL_NONE = 0;
	/**
	 * The contant used by {@link #TAG_GAIN_CONTROL} to denote low gain up.
	 */
	public static final short GAIN_CONTROL_LOW_GAIN_UP = 1;
	/**
	 * The contant used by {@link #TAG_GAIN_CONTROL} to denote high gain up.
	 */
	public static final short GAIN_CONTROL_HIGH_GAIN_UP = 2;
	/**
	 * The contant used by {@link #TAG_GAIN_CONTROL} to denote low gain down.
	 */
	public static final short GAIN_CONTROL_LOW_GAIN_DOWN = 3;
	/**
	 * The contant used by {@link #TAG_GAIN_CONTROL} to denote high gain down.
	 */
	public static final short GAIN_CONTROL_HIGH_GAIN_DOWN = 4;

	/**
	 * The contant used by {@link #TAG_CONTRAST} to denote normal contrast.
	 */
	public static final short CONTRAST_NORMAL = 0;
	/**
	 * The contant used by {@link #TAG_CONTRAST} to denote soft contrast.
	 */
	public static final short CONTRAST_SOFT = 1;
	/**
	 * The contant used by {@link #TAG_CONTRAST} to denote hard contrast.
	 */
	public static final short CONTRAST_HARD = 2;

	/**
	 * The contant used by {@link #TAG_SATURATION} to denote normal saturation.
	 */
	public static final short SATURATION_NORMAL = 0;
	/**
	 * The contant used by {@link #TAG_SATURATION} to denote low saturation.
	 */
	public static final short SATURATION_LOW = 0;
	/**
	 * The contant used by {@link #TAG_SHARPNESS} to denote high saturation.
	 */
	public static final short SATURATION_HIGH = 0;

	/**
	 * The contant used by {@link #TAG_SHARPNESS} to denote normal sharpness.
	 */
	public static final short SHARPNESS_NORMAL = 0;
	/**
	 * The contant used by {@link #TAG_SHARPNESS} to denote soft sharpness.
	 */
	public static final short SHARPNESS_SOFT = 1;
	/**
	 * The contant used by {@link #TAG_SHARPNESS} to denote hard sharpness.
	 */
	public static final short SHARPNESS_HARD = 2;

	/**
	 * The contant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
	 * is unknown.
	 */
	public static final short SUBJECT_DISTANCE_RANGE_UNKNOWN = 0;
	/**
	 * The contant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
	 * is Macro.
	 */
	public static final short SUBJECT_DISTANCE_RANGE_MACRO = 1;
	/**
	 * The contant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
	 * is Close view.
	 */
	public static final short SUBJECT_DISTANCE_RANGE_CLOSE_VIEW = 2;
	/**
	 * The contant used by {@link #TAG_SUBJECT_DISTANCE_RANGE} to denote the subject distance range
	 * is Distant view.
	 */
	public static final short SUBJECT_DISTANCE_RANGE_DISTANT_VIEW = 3;

	/**
	 * The contant used by GPS latitude-related tags to denote the latitude is North latitude.
	 *
	 * @see #TAG_GPS_LATITUDE_REF
	 * @see #TAG_GPS_DEST_LATITUDE_REF
	 */
	public static final String LATITUDE_NORTH = "N";
	/**
	 * The contant used by GPS latitude-related tags to denote the latitude is South latitude.
	 *
	 * @see #TAG_GPS_LATITUDE_REF
	 * @see #TAG_GPS_DEST_LATITUDE_REF
	 */
	public static final String LATITUDE_SOUTH = "S";

	/**
	 * The contant used by GPS longitude-related tags to denote the longitude is East longitude.
	 *
	 * @see #TAG_GPS_LONGITUDE_REF
	 * @see #TAG_GPS_DEST_LONGITUDE_REF
	 */
	public static final String LONGITUDE_EAST = "E";
	/**
	 * The contant used by GPS longitude-related tags to denote the longitude is West longitude.
	 *
	 * @see #TAG_GPS_LONGITUDE_REF
	 * @see #TAG_GPS_DEST_LONGITUDE_REF
	 */
	public static final String LONGITUDE_WEST = "W";

	/**
	 * The contant used by {@link #TAG_GPS_ALTITUDE_REF} to denote the altitude is above sea level.
	 */
	public static final short ALTITUDE_ABOVE_SEA_LEVEL = 0;
	/**
	 * The contant used by {@link #TAG_GPS_ALTITUDE_REF} to denote the altitude is below sea level.
	 */
	public static final short ALTITUDE_BELOW_SEA_LEVEL = 1;

	/**
	 * The contant used by {@link #TAG_GPS_STATUS} to denote GPS measurement is in progress.
	 */
	public static final String GPS_MEASUREMENT_IN_PROGRESS = "A";
	/**
	 * The contant used by {@link #TAG_GPS_STATUS} to denote GPS measurement is interrupted.
	 */
	public static final String GPS_MEASUREMENT_INTERRUPTED = "V";

	/**
	 * The contant used by {@link #TAG_GPS_MEASURE_MODE} to denote GPS measurement is 2-dimensional.
	 */
	public static final String GPS_MEASUREMENT_2D = "2";
	/**
	 * The contant used by {@link #TAG_GPS_MEASURE_MODE} to denote GPS measurement is 3-dimensional.
	 */
	public static final String GPS_MEASUREMENT_3D = "3";

	/**
	 * The contant used by {@link #TAG_GPS_SPEED_REF} to denote the speed unit is kilometers per
	 * hour.
	 */
	public static final String GPS_SPEED_KILOMETERS_PER_HOUR = "K";
	/**
	 * The contant used by {@link #TAG_GPS_SPEED_REF} to denote the speed unit is miles per hour.
	 */
	public static final String GPS_SPEED_MILES_PER_HOUR = "M";
	/**
	 * The contant used by {@link #TAG_GPS_SPEED_REF} to denote the speed unit is knots.
	 */
	public static final String GPS_SPEED_KNOTS = "N";

	/**
	 * The contant used by GPS attributes to denote the direction is true direction.
	 */
	public static final String GPS_DIRECTION_TRUE = "T";
	/**
	 * The contant used by GPS attributes to denote the direction is magnetic direction.
	 */
	public static final String GPS_DIRECTION_MAGNETIC = "M";

	/**
	 * The contant used by {@link #TAG_GPS_DEST_DISTANCE_REF} to denote the distance unit is
	 * kilometers.
	 */
	public static final String GPS_DISTANCE_KILOMETERS = "K";
	/**
	 * The contant used by {@link #TAG_GPS_DEST_DISTANCE_REF} to denote the distance unit is miles.
	 */
	public static final String GPS_DISTANCE_MILES = "M";
	/**
	 * The contant used by {@link #TAG_GPS_DEST_DISTANCE_REF} to denote the distance unit is
	 * nautical miles.
	 */
	public static final String GPS_DISTANCE_NAUTICAL_MILES = "N";

	/**
	 * The contant used by {@link #TAG_GPS_DIFFERENTIAL} to denote no differential correction is
	 * applied.
	 */
	public static final short GPS_MEASUREMENT_NO_DIFFERENTIAL = 0;
	/**
	 * The contant used by {@link #TAG_GPS_DIFFERENTIAL} to denote differential correction is
	 * applied.
	 */
	public static final short GPS_MEASUREMENT_DIFFERENTIAL_CORRECTED = 1;

	/**
	 * The constant used by {@link #TAG_COMPRESSION} to denote the image is not compressed.
	 */
	public static final int DATA_UNCOMPRESSED = 1;
	/**
	 * The constant used by {@link #TAG_COMPRESSION} to denote the image is huffman compressed.
	 */
	public static final int DATA_HUFFMAN_COMPRESSED = 2;
	/**
	 * The constant used by {@link #TAG_COMPRESSION} to denote the image is JPEG.
	 */
	public static final int DATA_JPEG = 6;
	/**
	 * The constant used by {@link #TAG_COMPRESSION}, see DNG Specification 1.4.0.0.
	 * Section 3, Compression
	 */
	public static final int DATA_JPEG_COMPRESSED = 7;
	/**
	 * The constant used by {@link #TAG_COMPRESSION}, see DNG Specification 1.4.0.0.
	 * Section 3, Compression
	 */
	public static final int DATA_DEFLATE_ZIP = 8;
	/**
	 * The constant used by {@link #TAG_COMPRESSION} to denote the image is pack-bits compressed.
	 */
	public static final int DATA_PACK_BITS_COMPRESSED = 32773;
	/**
	 * The constant used by {@link #TAG_COMPRESSION}, see DNG Specification 1.4.0.0.
	 * Section 3, Compression
	 */
	public static final int DATA_LOSSY_JPEG = 34892;

	/**
	 * The constant used by {@link #TAG_BITS_PER_SAMPLE}.
	 * See JEITA CP-3451C Spec Section 6, Differences from Palette Color Images
	 */
	protected static final int[] BITS_PER_SAMPLE_RGB = new int[] { 8, 8, 8 };
	/**
	 * The constant used by {@link #TAG_BITS_PER_SAMPLE}.
	 * See JEITA CP-3451C Spec Section 4, Differences from Bilevel Images
	 */
	protected static final int[] BITS_PER_SAMPLE_GREYSCALE_1 = new int[] { 4 };
	/**
	 * The constant used by {@link #TAG_BITS_PER_SAMPLE}.
	 * See JEITA CP-3451C Spec Section 4, Differences from Bilevel Images
	 */
	protected static final int[] BITS_PER_SAMPLE_GREYSCALE_2 = new int[] { 8 };

	/**
	 * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
	 */
	public static final int PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO = 0;
	/**
	 * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
	 */
	public static final int PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO = 1;
	/**
	 * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
	 */
	public static final int PHOTOMETRIC_INTERPRETATION_RGB = 2;
	/**
	 * The constant used by {@link #TAG_PHOTOMETRIC_INTERPRETATION}.
	 */
	public static final int PHOTOMETRIC_INTERPRETATION_YCBCR = 6;

	/**
	 * The constant used by {@link #TAG_NEW_SUBFILE_TYPE}. See JEITA CP-3451C Spec Section 8.
	 */
	public static final int ORIGINAL_RESOLUTION_IMAGE = 0;
	/**
	 * The constant used by {@link #TAG_NEW_SUBFILE_TYPE}. See JEITA CP-3451C Spec Section 8.
	 */
	public static final int REDUCED_RESOLUTION_IMAGE = 1;

	// Maximum size for checking file type signature (see image_type_recognition_lite.cc)
	private static final int SIGNATURE_CHECK_SIZE = 5000;

	static final byte[] JPEG_SIGNATURE = new byte[] {(byte) 0xff, (byte) 0xd8, (byte) 0xff};
	private static final String RAF_SIGNATURE = "FUJIFILMCCD-RAW";
	private static final int RAF_OFFSET_TO_JPEG_IMAGE_OFFSET = 84;
	private static final int RAF_INFO_SIZE = 160;
	private static final int RAF_JPEG_LENGTH_VALUE_SIZE = 4;

	private static final byte[] HEIF_TYPE_FTYP = new byte[] {'f', 't', 'y', 'p'};
	private static final byte[] HEIF_BRAND_MIF1 = new byte[] {'m', 'i', 'f', '1'};
	private static final byte[] HEIF_BRAND_HEIC = new byte[] {'h', 'e', 'i', 'c'};

	// See http://fileformats.archiveteam.org/wiki/Olympus_ORF
	private static final short ORF_SIGNATURE_1 = 0x4f52;
	private static final short ORF_SIGNATURE_2 = 0x5352;
	// There are two formats for Olympus Makernote Headers. Each has different identifiers and
	// offsets to the actual data.
	// See http://www.exiv2.org/makernote.html#R1
	private static final byte[] ORF_MAKER_NOTE_HEADER_1 = new byte[] {(byte) 0x4f, (byte) 0x4c,
			(byte) 0x59, (byte) 0x4d, (byte) 0x50, (byte) 0x00}; // "OLYMP\0"
	private static final byte[] ORF_MAKER_NOTE_HEADER_2 = new byte[] {(byte) 0x4f, (byte) 0x4c,
			(byte) 0x59, (byte) 0x4d, (byte) 0x50, (byte) 0x55, (byte) 0x53, (byte) 0x00,
			(byte) 0x49, (byte) 0x49}; // "OLYMPUS\0II"
	private static final int ORF_MAKER_NOTE_HEADER_1_SIZE = 8;
	private static final int ORF_MAKER_NOTE_HEADER_2_SIZE = 12;

	// See http://fileformats.archiveteam.org/wiki/RW2
	private static final short RW2_SIGNATURE = 0x0055;

	// See http://fileformats.archiveteam.org/wiki/Pentax_PEF
	private static final String PEF_SIGNATURE = "PENTAX";
	// See http://www.exiv2.org/makernote.html#R11
	private static final int PEF_MAKER_NOTE_SKIP_SIZE = 6;

	// See PNG (Portable Network Graphics) Specification, Version 1.2,
	// 3.1. PNG file signature
	private static final byte[] PNG_SIGNATURE = new byte[] {(byte) 0x89, (byte) 0x50, (byte) 0x4e,
			(byte) 0x47, (byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a};
	// See PNG (Portable Network Graphics) Specification, Version 1.2,
	// 3.7. eXIf Exchangeable Image File (Exif) Profile
	private static final byte[] PNG_CHUNK_TYPE_EXIF = new byte[]{(byte) 0x65, (byte) 0x58,
			(byte) 0x49, (byte) 0x66};
	private static final byte[] PNG_CHUNK_TYPE_IHDR = new byte[]{(byte) 0x49, (byte) 0x48,
			(byte) 0x44, (byte) 0x52};
	private static final byte[] PNG_CHUNK_TYPE_IEND = new byte[]{(byte) 0x49, (byte) 0x45,
			(byte) 0x4e, (byte) 0x44};
	private static final int PNG_CHUNK_TYPE_BYTE_LENGTH = 4;
	private static final int PNG_CHUNK_CRC_BYTE_LENGTH = 4;
	private static final int PNG_OFFSET_TO_IHDR_BYTES = 12;

	// See https://developers.google.com/speed/webp/docs/riff_container, Section "WebP File Header"
	private static final byte[] WEBP_SIGNATURE_1 = new byte[] {'R', 'I', 'F', 'F'};
	private static final byte[] WEBP_SIGNATURE_2 = new byte[] {'W', 'E', 'B', 'P'};
	private static final int WEBP_FILE_SIZE_BYTE_LENGTH = 4;
	private static final byte[] WEBP_CHUNK_TYPE_EXIF = new byte[]{(byte) 0x45, (byte) 0x58,
			(byte) 0x49, (byte) 0x46};
	private static final int WEBP_CHUNK_TYPE_BYTE_LENGTH = 4;
	private static final int WEBP_CHUNK_SIZE_BYTE_LENGTH = 4;

	private static SimpleDateFormat sFormatter;

	// See Exchangeable image file format for digital still cameras: Exif version 2.2.
	// The following values are for parsing EXIF data area. There are tag groups in EXIF data area.
	// They are called "Image File Directory". They have multiple data formats to cover various
	// image metadata from GPS longitude to camera model name.

	// Types of Exif byte alignments (see JEITA CP-3451C Section 4.5.2)
	static final short BYTE_ALIGN_II = 0x4949;  // II: Intel order
	static final short BYTE_ALIGN_MM = 0x4d4d;  // MM: Motorola order

	// TIFF Header Fixed Constant (see JEITA CP-3451C Section 4.5.2)
	static final byte START_CODE = 0x2a; // 42
	private static final int IFD_OFFSET = 8;

	// Formats for the value in IFD entry (See TIFF 6.0 Section 2, "Image File Directory".)
	private static final int IFD_FORMAT_BYTE = 1;
	private static final int IFD_FORMAT_STRING = 2;
	private static final int IFD_FORMAT_USHORT = 3;
	private static final int IFD_FORMAT_ULONG = 4;
	private static final int IFD_FORMAT_URATIONAL = 5;
	private static final int IFD_FORMAT_SBYTE = 6;
	private static final int IFD_FORMAT_UNDEFINED = 7;
	private static final int IFD_FORMAT_SSHORT = 8;
	private static final int IFD_FORMAT_SLONG = 9;
	private static final int IFD_FORMAT_SRATIONAL = 10;
	private static final int IFD_FORMAT_SINGLE = 11;
	private static final int IFD_FORMAT_DOUBLE = 12;
	// Format indicating a new IFD entry (See Adobe PageMaker® 6.0 TIFF Technical Notes, "New Tag")
	private static final int IFD_FORMAT_IFD = 13;
	// Names for the data formats for debugging purpose.
	static final String[] IFD_FORMAT_NAMES = new String[] {
			"", "BYTE", "STRING", "USHORT", "ULONG", "URATIONAL", "SBYTE", "UNDEFINED", "SSHORT",
			"SLONG", "SRATIONAL", "SINGLE", "DOUBLE"
	};
	// Sizes of the components of each IFD value format
	static final int[] IFD_FORMAT_BYTES_PER_FORMAT = new int[] {
			0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 1
	};

	@SuppressWarnings("WeakerAccess") /* synthetic access */
	static final byte[] EXIF_ASCII_PREFIX = new byte[] {
			0x41, 0x53, 0x43, 0x49, 0x49, 0x0, 0x0, 0x0
	};

	// A class for indicating EXIF rational type.
	private static class Rational {
		public final long numerator;
		public final long denominator;

		@SuppressWarnings("WeakerAccess") /* synthetic access */
		Rational(double value) {
			this((long) (value * 10000), 10000);
		}

		@SuppressWarnings("WeakerAccess") /* synthetic access */
		Rational(long numerator, long denominator) {
			// Handle erroneous case
			if (denominator == 0) {
				this.numerator = 0;
				this.denominator = 1;
				return;
			}
			this.numerator = numerator;
			this.denominator = denominator;
		}

		@Override
		public String toString() {
			return numerator + "/" + denominator;
		}

		public double calculate() {
			return (double) numerator / denominator;
		}
	}

	// A class for indicating EXIF attribute.
	public static class ExifAttribute {
		public final int format;
		public final int numberOfComponents;
		public final byte[] bytes;

		@SuppressWarnings("WeakerAccess") /* synthetic access */
		ExifAttribute(int format, int numberOfComponents, byte[] bytes) {
			this.format = format;
			this.numberOfComponents = numberOfComponents;
			this.bytes = bytes;
		}

		public static ExifAttribute createUShort(int[] values, ByteOrder byteOrder) {
			final ByteBuffer buffer = ByteBuffer.wrap(
					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_USHORT] * values.length]);
			buffer.order(byteOrder);
			for (int value : values) {
				buffer.putShort((short) value);
			}
			return new ExifAttribute(IFD_FORMAT_USHORT, values.length, buffer.array());
		}

		public static ExifAttribute createUShort(int value, ByteOrder byteOrder) {
			return createUShort(new int[] {value}, byteOrder);
		}

		public static ExifAttribute createULong(long[] values, ByteOrder byteOrder) {
			final ByteBuffer buffer = ByteBuffer.wrap(
					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_ULONG] * values.length]);
			buffer.order(byteOrder);
			for (long value : values) {
				buffer.putInt((int) value);
			}
			return new ExifAttribute(IFD_FORMAT_ULONG, values.length, buffer.array());
		}

		public static ExifAttribute createULong(long value, ByteOrder byteOrder) {
			return createULong(new long[] {value}, byteOrder);
		}

		public static ExifAttribute createSLong(int[] values, ByteOrder byteOrder) {
			final ByteBuffer buffer = ByteBuffer.wrap(
					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SLONG] * values.length]);
			buffer.order(byteOrder);
			for (int value : values) {
				buffer.putInt(value);
			}
			return new ExifAttribute(IFD_FORMAT_SLONG, values.length, buffer.array());
		}

		public static ExifAttribute createSLong(int value, ByteOrder byteOrder) {
			return createSLong(new int[] {value}, byteOrder);
		}

		public static ExifAttribute createByte(String value) {
			// Exception for GPSAltitudeRef tag
			if (value.length() == 1 && value.charAt(0) >= '0' && value.charAt(0) <= '1') {
				final byte[] bytes = new byte[] { (byte) (value.charAt(0) - '0') };
				return new ExifAttribute(IFD_FORMAT_BYTE, bytes.length, bytes);
			}
			final byte[] ascii = value.getBytes(US_ASCII);
			return new ExifAttribute(IFD_FORMAT_BYTE, ascii.length, ascii);
		}

		public static ExifAttribute createString(String value) {
			return createString(value, US_ASCII);
		}

		public static ExifAttribute createString(String value, Charset encoding) {
			final byte[] ascii = (value + '\0').getBytes(encoding);

			return new ExifAttribute(IFD_FORMAT_STRING, ascii.length, ascii);
		}

		public static ExifAttribute createURational(Rational[] values, ByteOrder byteOrder) {
			final ByteBuffer buffer = ByteBuffer.wrap(
					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_URATIONAL] * values.length]);
			buffer.order(byteOrder);
			for (Rational value : values) {
				buffer.putInt((int) value.numerator);
				buffer.putInt((int) value.denominator);
			}
			return new ExifAttribute(IFD_FORMAT_URATIONAL, values.length, buffer.array());
		}

		public static ExifAttribute createURational(Rational value, ByteOrder byteOrder) {
			return createURational(new Rational[] {value}, byteOrder);
		}

		public static ExifAttribute createSRational(Rational[] values, ByteOrder byteOrder) {
			final ByteBuffer buffer = ByteBuffer.wrap(
					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_SRATIONAL] * values.length]);
			buffer.order(byteOrder);
			for (Rational value : values) {
				buffer.putInt((int) value.numerator);
				buffer.putInt((int) value.denominator);
			}
			return new ExifAttribute(IFD_FORMAT_SRATIONAL, values.length, buffer.array());
		}

		public static ExifAttribute createSRational(Rational value, ByteOrder byteOrder) {
			return createSRational(new Rational[] {value}, byteOrder);
		}

		public static ExifAttribute createDouble(double[] values, ByteOrder byteOrder) {
			final ByteBuffer buffer = ByteBuffer.wrap(
					new byte[IFD_FORMAT_BYTES_PER_FORMAT[IFD_FORMAT_DOUBLE] * values.length]);
			buffer.order(byteOrder);
			for (double value : values) {
				buffer.putDouble(value);
			}
			return new ExifAttribute(IFD_FORMAT_DOUBLE, values.length, buffer.array());
		}

		public static ExifAttribute createDouble(double value, ByteOrder byteOrder) {
			return createDouble(new double[] {value}, byteOrder);
		}

		@Override
		public String toString() {
			return "(" + IFD_FORMAT_NAMES[format] + ", data length:" + bytes.length + ")";
		}

		@SuppressWarnings("WeakerAccess") /* synthetic access */
		Object getValue(ByteOrder byteOrder) {
			ByteOrderedDataInputStream inputStream = null;
			try {
				inputStream = new ByteOrderedDataInputStream(bytes);
				inputStream.setByteOrder(byteOrder);
				switch (format) {
					case IFD_FORMAT_BYTE:
					case IFD_FORMAT_SBYTE: {
						// Exception for GPSAltitudeRef tag
						if (bytes.length == 1 && bytes[0] >= 0 && bytes[0] <= 1) {
							return new String(new char[] { (char) (bytes[0] + '0') });
						}
						return new String(bytes, US_ASCII);
					}
					case IFD_FORMAT_UNDEFINED:
					case IFD_FORMAT_STRING: {
						int index = 0;
						if (numberOfComponents >= EXIF_ASCII_PREFIX.length) {
							boolean same = true;
							for (int i = 0; i < EXIF_ASCII_PREFIX.length; ++i) {
								if (bytes[i] != EXIF_ASCII_PREFIX[i]) {
									same = false;
									break;
								}
							}
							if (same) {
								index = EXIF_ASCII_PREFIX.length;
							}
						}

						StringBuilder stringBuilder = new StringBuilder();
						while (index < numberOfComponents) {
							int ch = bytes[index];
							if (ch == 0) {
								break;
							}
							if (ch >= 32) {
								stringBuilder.append((char) ch);
							} else {
								stringBuilder.append('?');
							}
							++index;
						}
						return stringBuilder.toString();
					}
					case IFD_FORMAT_USHORT: {
						final int[] values = new int[numberOfComponents];
						for (int i = 0; i < numberOfComponents; ++i) {
							values[i] = inputStream.readUnsignedShort();
						}
						return values;
					}
					case IFD_FORMAT_ULONG: {
						final long[] values = new long[numberOfComponents];
						for (int i = 0; i < numberOfComponents; ++i) {
							values[i] = inputStream.readUnsignedInt();
						}
						return values;
					}
					case IFD_FORMAT_URATIONAL: {
						final Rational[] values = new Rational[numberOfComponents];
						for (int i = 0; i < numberOfComponents; ++i) {
							final long numerator = inputStream.readUnsignedInt();
							final long denominator = inputStream.readUnsignedInt();
							values[i] = new Rational(numerator, denominator);
						}
						return values;
					}
					case IFD_FORMAT_SSHORT: {
						final int[] values = new int[numberOfComponents];
						for (int i = 0; i < numberOfComponents; ++i) {
							values[i] = inputStream.readShort();
						}
						return values;
					}
					case IFD_FORMAT_SLONG: {
						final int[] values = new int[numberOfComponents];
						for (int i = 0; i < numberOfComponents; ++i) {
							values[i] = inputStream.readInt();
						}
						return values;
					}
					case IFD_FORMAT_SRATIONAL: {
						final Rational[] values = new Rational[numberOfComponents];
						for (int i = 0; i < numberOfComponents; ++i) {
							final long numerator = inputStream.readInt();
							final long denominator = inputStream.readInt();
							values[i] = new Rational(numerator, denominator);
						}
						return values;
					}
					case IFD_FORMAT_SINGLE: {
						final double[] values = new double[numberOfComponents];
						for (int i = 0; i < numberOfComponents; ++i) {
							values[i] = inputStream.readFloat();
						}
						return values;
					}
					case IFD_FORMAT_DOUBLE: {
						final double[] values = new double[numberOfComponents];
						for (int i = 0; i < numberOfComponents; ++i) {
							values[i] = inputStream.readDouble();
						}
						return values;
					}
					default:
						return null;
				}
			} catch (IOException e) {
				logger.warn("IOException occurred during reading a value", e);
				return null;
			} finally {
				if (inputStream != null) {
					try {
						inputStream.close();
					} catch (IOException e) {
						logger.error("IOException occurred while closing InputStream", e);
					}
				}
			}
		}

		public double getDoubleValue(ByteOrder byteOrder) {
			Object value = getValue(byteOrder);
			if (value == null) {
				throw new NumberFormatException("NULL can't be converted to a double value");
			}
			if (value instanceof String) {
				return Double.parseDouble((String) value);
			}
			if (value instanceof long[]) {
				long[] array = (long[]) value;
				if (array.length == 1) {
					return array[0];
				}
				throw new NumberFormatException("There are more than one component");
			}
			if (value instanceof int[]) {
				int[] array = (int[]) value;
				if (array.length == 1) {
					return array[0];
				}
				throw new NumberFormatException("There are more than one component");
			}
			if (value instanceof double[]) {
				double[] array = (double[]) value;
				if (array.length == 1) {
					return array[0];
				}
				throw new NumberFormatException("There are more than one component");
			}
			if (value instanceof Rational[]) {
				Rational[] array = (Rational[]) value;
				if (array.length == 1) {
					return array[0].calculate();
				}
				throw new NumberFormatException("There are more than one component");
			}
			throw new NumberFormatException("Couldn't find a double value");
		}

		public int getIntValue(ByteOrder byteOrder) {
			Object value = getValue(byteOrder);
			if (value == null) {
				throw new NumberFormatException("NULL can't be converted to a integer value");
			}
			if (value instanceof String) {
				return Integer.parseInt((String) value);
			}
			if (value instanceof long[]) {
				long[] array = (long[]) value;
				if (array.length == 1) {
					return (int) array[0];
				}
				throw new NumberFormatException("There are more than one component");
			}
			if (value instanceof int[]) {
				int[] array = (int[]) value;
				if (array.length == 1) {
					return array[0];
				}
				throw new NumberFormatException("There are more than one component");
			}
			throw new NumberFormatException("Couldn't find a integer value");
		}

		public String getUTF8StringValue() {
			if (bytes != null && bytes.length > 0) {
				return new String(bytes, StandardCharsets.UTF_8);
			}
			return null;
		}

		public String getStringValue(ByteOrder byteOrder) {
			Object value = getValue(byteOrder);
			if (value == null) {
				return null;
			}
			if (value instanceof String) {
				return (String) value;
			}

			final StringBuilder stringBuilder = new StringBuilder();
			if (value instanceof long[]) {
				long[] array = (long[]) value;
				for (int i = 0; i < array.length; ++i) {
					stringBuilder.append(array[i]);
					if (i + 1 != array.length) {
						stringBuilder.append(",");
					}
				}
				return stringBuilder.toString();
			}
			if (value instanceof int[]) {
				int[] array = (int[]) value;
				for (int i = 0; i < array.length; ++i) {
					stringBuilder.append(array[i]);
					if (i + 1 != array.length) {
						stringBuilder.append(",");
					}
				}
				return stringBuilder.toString();
			}
			if (value instanceof double[]) {
				double[] array = (double[]) value;
				for (int i = 0; i < array.length; ++i) {
					stringBuilder.append(array[i]);
					if (i + 1 != array.length) {
						stringBuilder.append(",");
					}
				}
				return stringBuilder.toString();
			}
			if (value instanceof Rational[]) {
				Rational[] array = (Rational[]) value;
				for (int i = 0; i < array.length; ++i) {
					stringBuilder.append(array[i].numerator);
					stringBuilder.append('/');
					stringBuilder.append(array[i].denominator);
					if (i + 1 != array.length) {
						stringBuilder.append(",");
					}
				}
				return stringBuilder.toString();
			}
			return null;
		}

		public int size() {
			return IFD_FORMAT_BYTES_PER_FORMAT[format] * numberOfComponents;
		}
	}

	// A class for indicating EXIF tag.
	static class ExifTag {
		public final int number;
		public final String name;
		public final int primaryFormat;
		public final int secondaryFormat;

		@SuppressWarnings("WeakerAccess") /* synthetic access */
		ExifTag(String name, int number, int format) {
			this.name = name;
			this.number = number;
			this.primaryFormat = format;
			this.secondaryFormat = -1;
		}

		@SuppressWarnings("WeakerAccess") /* synthetic access */
		ExifTag(String name, int number, int primaryFormat, int secondaryFormat) {
			this.name = name;
			this.number = number;
			this.primaryFormat = primaryFormat;
			this.secondaryFormat = secondaryFormat;
		}

		@SuppressWarnings("WeakerAccess") /* synthetic access */
		boolean isFormatCompatible(int format) {
			if (primaryFormat == IFD_FORMAT_UNDEFINED || format == IFD_FORMAT_UNDEFINED) {
				return true;
			} else if (primaryFormat == format || secondaryFormat == format) {
				return true;
			} else if ((primaryFormat == IFD_FORMAT_ULONG || secondaryFormat == IFD_FORMAT_ULONG)
					&& format == IFD_FORMAT_USHORT) {
				return true;
			} else if ((primaryFormat == IFD_FORMAT_SLONG || secondaryFormat == IFD_FORMAT_SLONG)
					&& format == IFD_FORMAT_SSHORT) {
				return true;
			} else if ((primaryFormat == IFD_FORMAT_DOUBLE || secondaryFormat == IFD_FORMAT_DOUBLE)
					&& format == IFD_FORMAT_SINGLE) {
				return true;
			}
			return false;
		}
	}

	// Primary image IFD TIFF tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
	private static final ExifTag[] IFD_TIFF_TAGS = new ExifTag[] {
			// For below two, see TIFF 6.0 Spec Section 3: Bilevel Images.
			new ExifTag(TAG_NEW_SUBFILE_TYPE, 254, IFD_FORMAT_ULONG),
			new ExifTag(TAG_SUBFILE_TYPE, 255, IFD_FORMAT_ULONG),
			new ExifTag(TAG_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT),
			new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT),
			new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT),
			new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING),
			new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING),
			new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING),
			new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT),
			new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT),
			new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT),
			new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT),
			new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT),
			new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING),
			new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING),
			new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING),
			new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL),
			// See Adobe PageMaker® 6.0 TIFF Technical Notes, Note 1.
			new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG),
			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG),
			new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT),
			new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT),
			new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING),
			new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
			new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
			// RW2 file tags
			// See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/PanasonicRaw.html)
			new ExifTag(TAG_RW2_SENSOR_TOP_BORDER, 4, IFD_FORMAT_ULONG),
			new ExifTag(TAG_RW2_SENSOR_LEFT_BORDER, 5, IFD_FORMAT_ULONG),
			new ExifTag(TAG_RW2_SENSOR_BOTTOM_BORDER, 6, IFD_FORMAT_ULONG),
			new ExifTag(TAG_RW2_SENSOR_RIGHT_BORDER, 7, IFD_FORMAT_ULONG),
			new ExifTag(TAG_RW2_ISO, 23, IFD_FORMAT_USHORT),
			new ExifTag(TAG_RW2_JPG_FROM_RAW, 46, IFD_FORMAT_UNDEFINED)
	};

	// Primary image IFD Exif Private tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
	private static final ExifTag[] IFD_EXIF_TAGS = new ExifTag[] {
			new ExifTag(TAG_EXPOSURE_TIME, 33434, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_F_NUMBER, 33437, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_EXPOSURE_PROGRAM, 34850, IFD_FORMAT_USHORT),
			new ExifTag(TAG_SPECTRAL_SENSITIVITY, 34852, IFD_FORMAT_STRING),
			new ExifTag(TAG_PHOTOGRAPHIC_SENSITIVITY, 34855, IFD_FORMAT_USHORT),
			new ExifTag(TAG_OECF, 34856, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_EXIF_VERSION, 36864, IFD_FORMAT_STRING),
			new ExifTag(TAG_DATETIME_ORIGINAL, 36867, IFD_FORMAT_STRING),
			new ExifTag(TAG_DATETIME_DIGITIZED, 36868, IFD_FORMAT_STRING),
			new ExifTag(TAG_COMPONENTS_CONFIGURATION, 37121, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_COMPRESSED_BITS_PER_PIXEL, 37122, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_SHUTTER_SPEED_VALUE, 37377, IFD_FORMAT_SRATIONAL),
			new ExifTag(TAG_APERTURE_VALUE, 37378, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_BRIGHTNESS_VALUE, 37379, IFD_FORMAT_SRATIONAL),
			new ExifTag(TAG_EXPOSURE_BIAS_VALUE, 37380, IFD_FORMAT_SRATIONAL),
			new ExifTag(TAG_MAX_APERTURE_VALUE, 37381, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_SUBJECT_DISTANCE, 37382, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_METERING_MODE, 37383, IFD_FORMAT_USHORT),
			new ExifTag(TAG_LIGHT_SOURCE, 37384, IFD_FORMAT_USHORT),
			new ExifTag(TAG_FLASH, 37385, IFD_FORMAT_USHORT),
			new ExifTag(TAG_FOCAL_LENGTH, 37386, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_SUBJECT_AREA, 37396, IFD_FORMAT_USHORT),
			new ExifTag(TAG_MAKER_NOTE, 37500, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_USER_COMMENT, 37510, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_SUBSEC_TIME, 37520, IFD_FORMAT_STRING),
			new ExifTag(TAG_SUBSEC_TIME_ORIGINAL, 37521, IFD_FORMAT_STRING),
			new ExifTag(TAG_SUBSEC_TIME_DIGITIZED, 37522, IFD_FORMAT_STRING),
			new ExifTag(TAG_FLASHPIX_VERSION, 40960, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_COLOR_SPACE, 40961, IFD_FORMAT_USHORT),
			new ExifTag(TAG_PIXEL_X_DIMENSION, 40962, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_PIXEL_Y_DIMENSION, 40963, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_RELATED_SOUND_FILE, 40964, IFD_FORMAT_STRING),
			new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
			new ExifTag(TAG_FLASH_ENERGY, 41483, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_SPATIAL_FREQUENCY_RESPONSE, 41484, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_FOCAL_PLANE_X_RESOLUTION, 41486, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_FOCAL_PLANE_Y_RESOLUTION, 41487, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_FOCAL_PLANE_RESOLUTION_UNIT, 41488, IFD_FORMAT_USHORT),
			new ExifTag(TAG_SUBJECT_LOCATION, 41492, IFD_FORMAT_USHORT),
			new ExifTag(TAG_EXPOSURE_INDEX, 41493, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_SENSING_METHOD, 41495, IFD_FORMAT_USHORT),
			new ExifTag(TAG_FILE_SOURCE, 41728, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_SCENE_TYPE, 41729, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_CFA_PATTERN, 41730, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_CUSTOM_RENDERED, 41985, IFD_FORMAT_USHORT),
			new ExifTag(TAG_EXPOSURE_MODE, 41986, IFD_FORMAT_USHORT),
			new ExifTag(TAG_WHITE_BALANCE, 41987, IFD_FORMAT_USHORT),
			new ExifTag(TAG_DIGITAL_ZOOM_RATIO, 41988, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_FOCAL_LENGTH_IN_35MM_FILM, 41989, IFD_FORMAT_USHORT),
			new ExifTag(TAG_SCENE_CAPTURE_TYPE, 41990, IFD_FORMAT_USHORT),
			new ExifTag(TAG_GAIN_CONTROL, 41991, IFD_FORMAT_USHORT),
			new ExifTag(TAG_CONTRAST, 41992, IFD_FORMAT_USHORT),
			new ExifTag(TAG_SATURATION, 41993, IFD_FORMAT_USHORT),
			new ExifTag(TAG_SHARPNESS, 41994, IFD_FORMAT_USHORT),
			new ExifTag(TAG_DEVICE_SETTING_DESCRIPTION, 41995, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_SUBJECT_DISTANCE_RANGE, 41996, IFD_FORMAT_USHORT),
			new ExifTag(TAG_IMAGE_UNIQUE_ID, 42016, IFD_FORMAT_STRING),
			new ExifTag(TAG_DNG_VERSION, 50706, IFD_FORMAT_BYTE),
			new ExifTag(TAG_DEFAULT_CROP_SIZE, 50720, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG)
	};

	// Primary image IFD GPS Info tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
	private static final ExifTag[] IFD_GPS_TAGS = new ExifTag[] {
			new ExifTag(TAG_GPS_VERSION_ID, 0, IFD_FORMAT_BYTE),
			new ExifTag(TAG_GPS_LATITUDE_REF, 1, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_LATITUDE, 2, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_LONGITUDE_REF, 3, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_LONGITUDE, 4, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_ALTITUDE_REF, 5, IFD_FORMAT_BYTE),
			new ExifTag(TAG_GPS_ALTITUDE, 6, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_TIMESTAMP, 7, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_SATELLITES, 8, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_STATUS, 9, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_MEASURE_MODE, 10, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_DOP, 11, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_SPEED_REF, 12, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_SPEED, 13, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_TRACK_REF, 14, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_TRACK, 15, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_IMG_DIRECTION_REF, 16, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_IMG_DIRECTION, 17, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_MAP_DATUM, 18, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_DEST_LATITUDE_REF, 19, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_DEST_LATITUDE, 20, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_DEST_LONGITUDE_REF, 21, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_DEST_LONGITUDE, 22, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_DEST_BEARING_REF, 23, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_DEST_BEARING, 24, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_DEST_DISTANCE_REF, 25, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_DEST_DISTANCE, 26, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_GPS_PROCESSING_METHOD, 27, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_GPS_AREA_INFORMATION, 28, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_GPS_DATESTAMP, 29, IFD_FORMAT_STRING),
			new ExifTag(TAG_GPS_DIFFERENTIAL, 30, IFD_FORMAT_USHORT)
	};
	// Primary image IFD Interoperability tag (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
	private static final ExifTag[] IFD_INTEROPERABILITY_TAGS = new ExifTag[] {
			new ExifTag(TAG_INTEROPERABILITY_INDEX, 1, IFD_FORMAT_STRING)
	};
	// IFD Thumbnail tags (See JEITA CP-3451C Section 4.6.8 Tag Support Levels)
	private static final ExifTag[] IFD_THUMBNAIL_TAGS = new ExifTag[] {
			// For below two, see TIFF 6.0 Spec Section 3: Bilevel Images.
			new ExifTag(TAG_NEW_SUBFILE_TYPE, 254, IFD_FORMAT_ULONG),
			new ExifTag(TAG_SUBFILE_TYPE, 255, IFD_FORMAT_ULONG),
			new ExifTag(TAG_THUMBNAIL_IMAGE_WIDTH, 256, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_THUMBNAIL_IMAGE_LENGTH, 257, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_BITS_PER_SAMPLE, 258, IFD_FORMAT_USHORT),
			new ExifTag(TAG_COMPRESSION, 259, IFD_FORMAT_USHORT),
			new ExifTag(TAG_PHOTOMETRIC_INTERPRETATION, 262, IFD_FORMAT_USHORT),
			new ExifTag(TAG_IMAGE_DESCRIPTION, 270, IFD_FORMAT_STRING),
			new ExifTag(TAG_MAKE, 271, IFD_FORMAT_STRING),
			new ExifTag(TAG_MODEL, 272, IFD_FORMAT_STRING),
			new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_ORIENTATION, 274, IFD_FORMAT_USHORT),
			new ExifTag(TAG_SAMPLES_PER_PIXEL, 277, IFD_FORMAT_USHORT),
			new ExifTag(TAG_ROWS_PER_STRIP, 278, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_STRIP_BYTE_COUNTS, 279, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG),
			new ExifTag(TAG_X_RESOLUTION, 282, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_Y_RESOLUTION, 283, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_PLANAR_CONFIGURATION, 284, IFD_FORMAT_USHORT),
			new ExifTag(TAG_RESOLUTION_UNIT, 296, IFD_FORMAT_USHORT),
			new ExifTag(TAG_TRANSFER_FUNCTION, 301, IFD_FORMAT_USHORT),
			new ExifTag(TAG_SOFTWARE, 305, IFD_FORMAT_STRING),
			new ExifTag(TAG_DATETIME, 306, IFD_FORMAT_STRING),
			new ExifTag(TAG_ARTIST, 315, IFD_FORMAT_STRING),
			new ExifTag(TAG_WHITE_POINT, 318, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_PRIMARY_CHROMATICITIES, 319, IFD_FORMAT_URATIONAL),
			// See Adobe PageMaker® 6.0 TIFF Technical Notes, Note 1.
			new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG),
			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG),
			new ExifTag(TAG_Y_CB_CR_COEFFICIENTS, 529, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_Y_CB_CR_SUB_SAMPLING, 530, IFD_FORMAT_USHORT),
			new ExifTag(TAG_Y_CB_CR_POSITIONING, 531, IFD_FORMAT_USHORT),
			new ExifTag(TAG_REFERENCE_BLACK_WHITE, 532, IFD_FORMAT_URATIONAL),
			new ExifTag(TAG_COPYRIGHT, 33432, IFD_FORMAT_STRING),
			new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
			new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
			new ExifTag(TAG_DNG_VERSION, 50706, IFD_FORMAT_BYTE),
			new ExifTag(TAG_DEFAULT_CROP_SIZE, 50720, IFD_FORMAT_USHORT, IFD_FORMAT_ULONG)
	};

	// RAF file tag (See piex.cc line 372)
	private static final ExifTag TAG_RAF_IMAGE_SIZE =
			new ExifTag(TAG_STRIP_OFFSETS, 273, IFD_FORMAT_USHORT);

	// ORF file tags (See http://www.exiv2.org/tags-olympus.html)
	private static final ExifTag[] ORF_MAKER_NOTE_TAGS = new ExifTag[] {
			new ExifTag(TAG_ORF_THUMBNAIL_IMAGE, 256, IFD_FORMAT_UNDEFINED),
			new ExifTag(TAG_ORF_CAMERA_SETTINGS_IFD_POINTER, 8224, IFD_FORMAT_ULONG),
			new ExifTag(TAG_ORF_IMAGE_PROCESSING_IFD_POINTER, 8256, IFD_FORMAT_ULONG)
	};
	private static final ExifTag[] ORF_CAMERA_SETTINGS_TAGS = new ExifTag[] {
			new ExifTag(TAG_ORF_PREVIEW_IMAGE_START, 257, IFD_FORMAT_ULONG),
			new ExifTag(TAG_ORF_PREVIEW_IMAGE_LENGTH, 258, IFD_FORMAT_ULONG)
	};
	private static final ExifTag[] ORF_IMAGE_PROCESSING_TAGS = new ExifTag[] {
			new ExifTag(TAG_ORF_ASPECT_FRAME, 4371, IFD_FORMAT_USHORT)
	};
	// PEF file tag (See http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Pentax.html)
	private static final ExifTag[] PEF_TAGS = new ExifTag[] {
			new ExifTag(TAG_COLOR_SPACE, 55, IFD_FORMAT_USHORT)
	};

	// See JEITA CP-3451C Section 4.6.3: Exif-specific IFD.
	// The following values are used for indicating pointers to the other Image File Directories.

	// Indices of Exif Ifd tag groups
	/** @hide */
	@RestrictTo(RestrictTo.Scope.LIBRARY)
	@Retention(RetentionPolicy.SOURCE)
	@IntDef({IFD_TYPE_PRIMARY, IFD_TYPE_EXIF, IFD_TYPE_GPS, IFD_TYPE_INTEROPERABILITY,
			IFD_TYPE_THUMBNAIL, IFD_TYPE_PREVIEW, IFD_TYPE_ORF_MAKER_NOTE,
			IFD_TYPE_ORF_CAMERA_SETTINGS, IFD_TYPE_ORF_IMAGE_PROCESSING, IFD_TYPE_PEF})
	public @interface IfdType {}

	static final int IFD_TYPE_PRIMARY = 0;
	private static final int IFD_TYPE_EXIF = 1;
	private static final int IFD_TYPE_GPS = 2;
	private static final int IFD_TYPE_INTEROPERABILITY = 3;
	static final int IFD_TYPE_THUMBNAIL = 4;
	static final int IFD_TYPE_PREVIEW = 5;
	private static final int IFD_TYPE_ORF_MAKER_NOTE = 6;
	private static final int IFD_TYPE_ORF_CAMERA_SETTINGS = 7;
	private static final int IFD_TYPE_ORF_IMAGE_PROCESSING = 8;
	private static final int IFD_TYPE_PEF = 9;

	// List of Exif tag groups
	static final ExifTag[][] EXIF_TAGS = new ExifTag[][] {
			IFD_TIFF_TAGS, IFD_EXIF_TAGS, IFD_GPS_TAGS, IFD_INTEROPERABILITY_TAGS,
			IFD_THUMBNAIL_TAGS, IFD_TIFF_TAGS, ORF_MAKER_NOTE_TAGS, ORF_CAMERA_SETTINGS_TAGS,
			ORF_IMAGE_PROCESSING_TAGS, PEF_TAGS
	};
	// List of tags for pointing to the other image file directory offset.
	private static final ExifTag[] EXIF_POINTER_TAGS = new ExifTag[] {
			new ExifTag(TAG_SUB_IFD_POINTER, 330, IFD_FORMAT_ULONG),
			new ExifTag(TAG_EXIF_IFD_POINTER, 34665, IFD_FORMAT_ULONG),
			new ExifTag(TAG_GPS_INFO_IFD_POINTER, 34853, IFD_FORMAT_ULONG),
			new ExifTag(TAG_INTEROPERABILITY_IFD_POINTER, 40965, IFD_FORMAT_ULONG),
			new ExifTag(TAG_ORF_CAMERA_SETTINGS_IFD_POINTER, 8224, IFD_FORMAT_BYTE),
			new ExifTag(TAG_ORF_IMAGE_PROCESSING_IFD_POINTER, 8256, IFD_FORMAT_BYTE)
	};

	// Tags for indicating the thumbnail offset and length
	private static final ExifTag JPEG_INTERCHANGE_FORMAT_TAG =
			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT, 513, IFD_FORMAT_ULONG);
	private static final ExifTag JPEG_INTERCHANGE_FORMAT_LENGTH_TAG =
			new ExifTag(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, 514, IFD_FORMAT_ULONG);

	// Mappings from tag number to tag name and each item represents one IFD tag group.
	@SuppressWarnings("unchecked")
	private static final HashMap<Integer, ExifTag>[] sExifTagMapsForReading =
			new HashMap[EXIF_TAGS.length];
	// Mappings from tag name to tag number and each item represents one IFD tag group.
	@SuppressWarnings("unchecked")
	private static final HashMap<String, ExifTag>[] sExifTagMapsForWriting =
			new HashMap[EXIF_TAGS.length];
	private static final HashSet<String> sTagSetForCompatibility = new HashSet<>(Arrays.asList(
			TAG_F_NUMBER, TAG_DIGITAL_ZOOM_RATIO, TAG_EXPOSURE_TIME, TAG_SUBJECT_DISTANCE,
			TAG_GPS_TIMESTAMP));
	// Mappings from tag number to IFD type for pointer tags.
	@SuppressWarnings("unchecked")
	private static final HashMap<Integer, Integer> sExifPointerTagMap = new HashMap();

	// See JPEG File Interchange Format Version 1.02.
	// The following values are defined for handling JPEG streams. In this implementation, we are
	// not only getting information from EXIF but also from some JPEG special segments such as
	// MARKER_COM for user comment and MARKER_SOFx for image width and height.
	//
	// Identifier for EXIF APP1 segment in JPEG
	static final byte[] IDENTIFIER_EXIF_APP1 = "Exif\0\0".getBytes(US_ASCII);
	// JPEG segment markers, that each marker consumes two bytes beginning with 0xff and ending with
	// the indicator. There is no SOF4, SOF8, SOF16 markers in JPEG and SOFx markers indicates start
	// of frame(baseline DCT) and the image size info exists in its beginning part.
	static final byte MARKER = (byte) 0xff;
	private static final byte MARKER_SOI = (byte) 0xd8;
	private static final byte MARKER_SOF0 = (byte) 0xc0;
	private static final byte MARKER_SOF1 = (byte) 0xc1;
	private static final byte MARKER_SOF2 = (byte) 0xc2;
	private static final byte MARKER_SOF3 = (byte) 0xc3;
	private static final byte MARKER_SOF5 = (byte) 0xc5;
	private static final byte MARKER_SOF6 = (byte) 0xc6;
	private static final byte MARKER_SOF7 = (byte) 0xc7;
	private static final byte MARKER_SOF9 = (byte) 0xc9;
	private static final byte MARKER_SOF10 = (byte) 0xca;
	private static final byte MARKER_SOF11 = (byte) 0xcb;
	private static final byte MARKER_SOF13 = (byte) 0xcd;
	private static final byte MARKER_SOF14 = (byte) 0xce;
	private static final byte MARKER_SOF15 = (byte) 0xcf;
	private static final byte MARKER_SOS = (byte) 0xda;
	static final byte MARKER_APP1 = (byte) 0xe1;
	private static final byte MARKER_COM = (byte) 0xfe;
	static final byte MARKER_EOI = (byte) 0xd9;

	// Supported Image File Types
	private static final int IMAGE_TYPE_UNKNOWN = 0;
	private static final int IMAGE_TYPE_ARW = 1;
	private static final int IMAGE_TYPE_CR2 = 2;
	private static final int IMAGE_TYPE_DNG = 3;
	private static final int IMAGE_TYPE_JPEG = 4;
	private static final int IMAGE_TYPE_NEF = 5;
	private static final int IMAGE_TYPE_NRW = 6;
	private static final int IMAGE_TYPE_ORF = 7;
	private static final int IMAGE_TYPE_PEF = 8;
	private static final int IMAGE_TYPE_RAF = 9;
	private static final int IMAGE_TYPE_RW2 = 10;
	private static final int IMAGE_TYPE_SRW = 11;
	private static final int IMAGE_TYPE_HEIF = 12;
	private static final int IMAGE_TYPE_PNG = 13;
	private static final int IMAGE_TYPE_WEBP = 14;

	static {
		sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
		sFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));

		// Build up the hash tables to look up Exif tags for reading Exif tags.
		for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
			sExifTagMapsForReading[ifdType] = new HashMap<>();
			sExifTagMapsForWriting[ifdType] = new HashMap<>();
			for (ExifTag tag : EXIF_TAGS[ifdType]) {
				sExifTagMapsForReading[ifdType].put(tag.number, tag);
				sExifTagMapsForWriting[ifdType].put(tag.name, tag);
			}
		}

		// Build up the hash table to look up Exif pointer tags.
		sExifPointerTagMap.put(EXIF_POINTER_TAGS[0].number, IFD_TYPE_PREVIEW); // 330
		sExifPointerTagMap.put(EXIF_POINTER_TAGS[1].number, IFD_TYPE_EXIF); // 34665
		sExifPointerTagMap.put(EXIF_POINTER_TAGS[2].number, IFD_TYPE_GPS); // 34853
		sExifPointerTagMap.put(EXIF_POINTER_TAGS[3].number, IFD_TYPE_INTEROPERABILITY); // 40965
		sExifPointerTagMap.put(EXIF_POINTER_TAGS[4].number, IFD_TYPE_ORF_CAMERA_SETTINGS); // 8224
		sExifPointerTagMap.put(EXIF_POINTER_TAGS[5].number, IFD_TYPE_ORF_IMAGE_PROCESSING); // 8256
	}

	private final String mFilename;
	private final AssetManager.AssetInputStream mAssetInputStream;
	private int mMimeType;
	@SuppressWarnings("unchecked")
	private final HashMap<String, ExifAttribute>[] mAttributes = new HashMap[EXIF_TAGS.length];
	private Set<Integer> mAttributesOffsets = new HashSet<>(EXIF_TAGS.length);
	private ByteOrder mExifByteOrder = ByteOrder.BIG_ENDIAN;
	private boolean mHasThumbnail;
	// The following values used for indicating a thumbnail position.
	private int mThumbnailOffset;
	private int mThumbnailLength;
	private byte[] mThumbnailBytes;
	private int mThumbnailCompression;
	private int mExifOffset;
	private int mOrfMakerNoteOffset;
	private int mOrfThumbnailOffset;
	private int mOrfThumbnailLength;
	private int mRw2JpgFromRawOffset;
	private boolean mIsSupportedFile;

	// Pattern to check non zero timestamp
	private static final Pattern sNonZeroTimePattern = Pattern.compile(".*[1-9].*");
	// Pattern to check gps timestamp
	private static final Pattern sGpsTimestampPattern =
			Pattern.compile("^([0-9][0-9]):([0-9][0-9]):([0-9][0-9])$");

	/**
	 * Reads Exif tags from the specified image file.
	 */
	public ExifInterface(@NonNull String filename) throws IOException {
		if (filename == null) {
			throw new IllegalArgumentException("filename cannot be null");
		}
		FileInputStream in = null;
		mAssetInputStream = null;
		mFilename = filename;
		try {
			in = new FileInputStream(filename);
			loadAttributes(in);
		} finally {
			closeQuietly(in);
		}
	}

	/**
	 * Reads Exif tags from the specified image input stream. Attribute mutation is not supported
	 * for input streams. The given input stream will proceed its current position. Developers
	 * should close the input stream after use. This constructor is not intended to be used with
	 * an input stream that performs any networking operations.
	 */
	public ExifInterface(@NonNull InputStream inputStream) throws IOException {
		if (inputStream == null) {
			throw new IllegalArgumentException("inputStream cannot be null");
		}
		mFilename = null;
		if (inputStream instanceof AssetManager.AssetInputStream) {
			mAssetInputStream = (AssetManager.AssetInputStream) inputStream;
		} else {
			mAssetInputStream = null;
		}
		loadAttributes(inputStream);
	}

	/**
	 * Returns whether ExifInterface currently supports parsing data from the specified mime type
	 * or not.
	 *
	 * @param mimeType the string value of mime type
	 */
	public static boolean isSupportedMimeType(@NonNull String mimeType) {
		if (mimeType == null) {
			throw new NullPointerException("mimeType shouldn't be null");
		}

		switch (mimeType.toLowerCase(Locale.ROOT)) {
			case "image/jpeg":
			case "image/x-adobe-dng":
			case "image/x-canon-cr2":
			case "image/x-nikon-nef":
			case "image/x-nikon-nrw":
			case "image/x-sony-arw":
			case "image/x-panasonic-rw2":
			case "image/x-olympus-orf":
			case "image/x-pentax-pef":
			case "image/x-samsung-srw":
			case "image/x-fuji-raf":
			case "image/heic":
			case "image/heif":
				return true;
			default:
				return false;
		}
	}

	/**
	 * Returns the EXIF attribute of the specified tag or {@code null} if there is no such tag in
	 * the image file.
	 *
	 * @param tag the name of the tag.
	 */
	@Nullable
	public ExifAttribute getExifAttribute(@NonNull String tag) {
		if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
			if (DEBUG) {
				logger.debug("getExifAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
						+ "TAG_PHOTOGRAPHIC_SENSITIVITY.");
			}
			tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
		}
		// Retrieves all tag groups. The value from primary image tag group has a higher priority
		// than the value from the thumbnail tag group if there are more than one candidates.
		for (int i = 0; i < EXIF_TAGS.length; ++i) {
			ExifAttribute value = mAttributes[i].get(tag);
			if (value != null) {
				return value;
			}
		}
		return null;
	}

	/**
	 * Returns the value of the specified tag assuming it's a UTF-8 string or {@code null} if there
	 * is no such tag in the image file.
	 * @param tag
	 * @return
	 */
	@Nullable
	public String getUTF8StringAttribute(@NonNull String tag) {
		ExifAttribute attribute = getExifAttribute(tag);
		if (attribute != null) {
			return attribute.getUTF8StringValue();
		}
		return null;
	}

	/**
	 * Returns the value of the specified tag or {@code null} if there
	 * is no such tag in the image file.
	 *
	 * @param tag the name of the tag.
	 */
	@Nullable
	public String getAttribute(@NonNull String tag) {
		ExifAttribute attribute = getExifAttribute(tag);
		if (attribute != null) {
			if (!sTagSetForCompatibility.contains(tag)) {
				return attribute.getStringValue(mExifByteOrder);
			}
			if (tag.equals(TAG_GPS_TIMESTAMP)) {
				// Convert the rational values to the custom formats for backwards compatibility.
				if (attribute.format != IFD_FORMAT_URATIONAL
						&& attribute.format != IFD_FORMAT_SRATIONAL) {
					logger.warn("GPS Timestamp format is not rational. format=" + attribute.format);
					return null;
				}
				Rational[] array = (Rational[]) attribute.getValue(mExifByteOrder);
				if (array == null || array.length != 3) {
					logger.warn("Invalid GPS Timestamp array. array=" + Arrays.toString(array));
					return null;
				}
				return String.format("%02d:%02d:%02d",
						(int) ((float) array[0].numerator / array[0].denominator),
						(int) ((float) array[1].numerator / array[1].denominator),
						(int) ((float) array[2].numerator / array[2].denominator));
			}
			try {
				return Double.toString(attribute.getDoubleValue(mExifByteOrder));
			} catch (NumberFormatException e) {
				return null;
			}
		}
		return null;
	}

	/**
	 * Returns the integer value of the specified tag. If there is no such tag
	 * in the image file or the value cannot be parsed as integer, return
	 * <var>defaultValue</var>.
	 *
	 * @param tag the name of the tag.
	 * @param defaultValue the value to return if the tag is not available.
	 */
	public int getAttributeInt(@NonNull String tag, int defaultValue) {
		ExifAttribute exifAttribute = getExifAttribute(tag);
		if (exifAttribute == null) {
			return defaultValue;
		}

		try {
			return exifAttribute.getIntValue(mExifByteOrder);
		} catch (NumberFormatException e) {
			return defaultValue;
		}
	}

	/**
	 * Returns the double value of the tag that is specified as rational or contains a
	 * double-formatted value. If there is no such tag in the image file or the value cannot be
	 * parsed as double, return <var>defaultValue</var>.
	 *
	 * @param tag the name of the tag.
	 * @param defaultValue the value to return if the tag is not available.
	 */
	public double getAttributeDouble(@NonNull String tag, double defaultValue) {
		ExifAttribute exifAttribute = getExifAttribute(tag);
		if (exifAttribute == null) {
			return defaultValue;
		}

		try {
			return exifAttribute.getDoubleValue(mExifByteOrder);
		} catch (NumberFormatException e) {
			return defaultValue;
		}
	}

	/**
	 * Sets the value of the specified tag.
	 *
	 * @param tag the name of the tag.
	 * @param value the value of the tag.
	 */
	public void setAttribute(@NonNull String tag, @Nullable String value) {
		setAttribute(tag, value, US_ASCII);
	}

	/**
	 * Sets the value of the specified tag.
	 *
	 * @param tag the name of the tag.
	 * @param value the value of the tag.
	 * @param encoding string charset, defaults to ASCII
	 */
	public void setAttribute(@NonNull String tag, @Nullable String value, Charset encoding) {
		if (TAG_ISO_SPEED_RATINGS.equals(tag)) {
			if (DEBUG) {
				logger.debug("setAttribute: Replacing TAG_ISO_SPEED_RATINGS with "
						+ "TAG_PHOTOGRAPHIC_SENSITIVITY.");
			}
			tag = TAG_PHOTOGRAPHIC_SENSITIVITY;
		}
		// Convert the given value to rational values for backwards compatibility.
		if (value != null && sTagSetForCompatibility.contains(tag)) {
			if (tag.equals(TAG_GPS_TIMESTAMP)) {
				Matcher m = sGpsTimestampPattern.matcher(value);
				if (!m.find()) {
					logger.warn("Invalid value for " + tag + " : " + value);
					return;
				}
				value = Integer.parseInt(m.group(1)) + "/1," + Integer.parseInt(m.group(2)) + "/1,"
						+ Integer.parseInt(m.group(3)) + "/1";
			} else {
				try {
					double doubleValue = Double.parseDouble(value);
					value = new Rational(doubleValue).toString();
				} catch (NumberFormatException e) {
					logger.warn("Invalid value for " + tag + " : " + value);
					return;
				}
			}
		}

		for (int i = 0 ; i < EXIF_TAGS.length; ++i) {
			if (i == IFD_TYPE_THUMBNAIL && !mHasThumbnail) {
				continue;
			}
			final ExifTag exifTag = sExifTagMapsForWriting[i].get(tag);
			if (exifTag != null) {
				if (value == null) {
					mAttributes[i].remove(tag);
					continue;
				}
				Pair<Integer, Integer> guess = guessDataFormat(value);
				int dataFormat;
				if (exifTag.primaryFormat == guess.first || exifTag.primaryFormat == guess.second) {
					dataFormat = exifTag.primaryFormat;
				} else if (exifTag.secondaryFormat != -1 && (exifTag.secondaryFormat == guess.first
						|| exifTag.secondaryFormat == guess.second)) {
					dataFormat = exifTag.secondaryFormat;
				} else if (exifTag.primaryFormat == IFD_FORMAT_BYTE
						|| exifTag.primaryFormat == IFD_FORMAT_UNDEFINED
						|| exifTag.primaryFormat == IFD_FORMAT_STRING) {
					dataFormat = exifTag.primaryFormat;
				} else {
					logger.warn("Given tag (" + tag + ") value didn't match with one of expected "
							+ "formats: " + IFD_FORMAT_NAMES[exifTag.primaryFormat]
							+ (exifTag.secondaryFormat == -1 ? "" : ", "
							+ IFD_FORMAT_NAMES[exifTag.secondaryFormat]) + " (guess: "
							+ IFD_FORMAT_NAMES[guess.first] + (guess.second == -1 ? "" : ", "
							+ IFD_FORMAT_NAMES[guess.second]) + ")");
					continue;
				}
				switch (dataFormat) {
					case IFD_FORMAT_BYTE: {
						mAttributes[i].put(tag, ExifAttribute.createByte(value));
						break;
					}
					case IFD_FORMAT_UNDEFINED:
					case IFD_FORMAT_STRING: {
						mAttributes[i].put(tag, ExifAttribute.createString(value, encoding));
						break;
					}
					case IFD_FORMAT_USHORT: {
						final String[] values = value.split(",", -1);
						final int[] intArray = new int[values.length];
						for (int j = 0; j < values.length; ++j) {
							intArray[j] = Integer.parseInt(values[j]);
						}
						mAttributes[i].put(tag,
								ExifAttribute.createUShort(intArray, mExifByteOrder));
						break;
					}
					case IFD_FORMAT_SLONG: {
						final String[] values = value.split(",", -1);
						final int[] intArray = new int[values.length];
						for (int j = 0; j < values.length; ++j) {
							intArray[j] = Integer.parseInt(values[j]);
						}
						mAttributes[i].put(tag,
								ExifAttribute.createSLong(intArray, mExifByteOrder));
						break;
					}
					case IFD_FORMAT_ULONG: {
						final String[] values = value.split(",", -1);
						final long[] longArray = new long[values.length];
						for (int j = 0; j < values.length; ++j) {
							longArray[j] = Long.parseLong(values[j]);
						}
						mAttributes[i].put(tag,
								ExifAttribute.createULong(longArray, mExifByteOrder));
						break;
					}
					case IFD_FORMAT_URATIONAL: {
						final String[] values = value.split(",", -1);
						final Rational[] rationalArray = new Rational[values.length];
						for (int j = 0; j < values.length; ++j) {
							final String[] numbers = values[j].split("/", -1);
							rationalArray[j] = new Rational((long) Double.parseDouble(numbers[0]),
									(long) Double.parseDouble(numbers[1]));
						}
						mAttributes[i].put(tag,
								ExifAttribute.createURational(rationalArray, mExifByteOrder));
						break;
					}
					case IFD_FORMAT_SRATIONAL: {
						final String[] values = value.split(",", -1);
						final Rational[] rationalArray = new Rational[values.length];
						for (int j = 0; j < values.length; ++j) {
							final String[] numbers = values[j].split("/", -1);
							rationalArray[j] = new Rational((long) Double.parseDouble(numbers[0]),
									(long) Double.parseDouble(numbers[1]));
						}
						mAttributes[i].put(tag,
								ExifAttribute.createSRational(rationalArray, mExifByteOrder));
						break;
					}
					case IFD_FORMAT_DOUBLE: {
						final String[] values = value.split(",", -1);
						final double[] doubleArray = new double[values.length];
						for (int j = 0; j < values.length; ++j) {
							doubleArray[j] = Double.parseDouble(values[j]);
						}
						mAttributes[i].put(tag,
								ExifAttribute.createDouble(doubleArray, mExifByteOrder));
						break;
					}
					default:
						logger.warn("Data format isn't one of expected formats: " + dataFormat);
						continue;
				}
			}
		}
	}

	/**
	 * Resets the {@link #TAG_ORIENTATION} of the image to be {@link #ORIENTATION_NORMAL}.
	 */
	public void resetOrientation() {
		setAttribute(TAG_ORIENTATION, Integer.toString(ORIENTATION_NORMAL));
	}

	/**
	 * Rotates the image by the given degree clockwise. The degree should be a multiple of
	 * 90 (e.g, 90, 180, -90, etc.).
	 *
	 * @param degree The degree of rotation.
	 */
	public void rotate(int degree) throws IllegalArgumentException {
		if (degree % 90 !=0) {
			throw new IllegalArgumentException("degree should be a multiple of 90");
		}

		int currentOrientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
		int currentIndex, newIndex;
		int resultOrientation;
		if (ROTATION_ORDER.contains(currentOrientation)) {
			currentIndex = ROTATION_ORDER.indexOf(currentOrientation);
			newIndex = (currentIndex + degree / 90) % 4;
			newIndex += newIndex < 0 ? 4 : 0;
			resultOrientation = ROTATION_ORDER.get(newIndex);
		} else if (FLIPPED_ROTATION_ORDER.contains(currentOrientation)) {
			currentIndex = FLIPPED_ROTATION_ORDER.indexOf(currentOrientation);
			newIndex = (currentIndex + degree / 90) % 4;
			newIndex += newIndex < 0 ? 4 : 0;
			resultOrientation = FLIPPED_ROTATION_ORDER.get(newIndex);
		} else {
			resultOrientation = ORIENTATION_UNDEFINED;
		}

		setAttribute(TAG_ORIENTATION, Integer.toString(resultOrientation));
	}

	/**
	 * Flips the image vertically.
	 */
	public void flipVertically() {
		int currentOrientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
		int resultOrientation;
		switch (currentOrientation) {
			case ORIENTATION_FLIP_HORIZONTAL:
				resultOrientation = ORIENTATION_ROTATE_180;
				break;
			case ORIENTATION_ROTATE_180:
				resultOrientation = ORIENTATION_FLIP_HORIZONTAL;
				break;
			case ORIENTATION_FLIP_VERTICAL:
				resultOrientation = ORIENTATION_NORMAL;
				break;
			case ORIENTATION_TRANSPOSE:
				resultOrientation = ORIENTATION_ROTATE_270;
				break;
			case ORIENTATION_ROTATE_90:
				resultOrientation = ORIENTATION_TRANSVERSE;
				break;
			case ORIENTATION_TRANSVERSE:
				resultOrientation = ORIENTATION_ROTATE_90;
				break;
			case ORIENTATION_ROTATE_270:
				resultOrientation = ORIENTATION_TRANSPOSE;
				break;
			case ORIENTATION_NORMAL:
				resultOrientation = ORIENTATION_FLIP_VERTICAL;
				break;
			case ORIENTATION_UNDEFINED:
			default:
				resultOrientation = ORIENTATION_UNDEFINED;
				break;
		}
		setAttribute(TAG_ORIENTATION, Integer.toString(resultOrientation));
	}

	/**
	 * Flips the image horizontally.
	 */
	public void flipHorizontally() {
		int currentOrientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
		int resultOrientation;
		switch (currentOrientation) {
			case ORIENTATION_FLIP_HORIZONTAL:
				resultOrientation = ORIENTATION_NORMAL;
				break;
			case ORIENTATION_ROTATE_180:
				resultOrientation = ORIENTATION_FLIP_VERTICAL;
				break;
			case ORIENTATION_FLIP_VERTICAL:
				resultOrientation = ORIENTATION_ROTATE_180;
				break;
			case ORIENTATION_TRANSPOSE:
				resultOrientation = ORIENTATION_ROTATE_90;
				break;
			case ORIENTATION_ROTATE_90:
				resultOrientation = ORIENTATION_TRANSPOSE;
				break;
			case ORIENTATION_TRANSVERSE:
				resultOrientation = ORIENTATION_ROTATE_270;
				break;
			case ORIENTATION_ROTATE_270:
				resultOrientation = ORIENTATION_TRANSVERSE;
				break;
			case ORIENTATION_NORMAL:
				resultOrientation = ORIENTATION_FLIP_HORIZONTAL;
				break;
			case ORIENTATION_UNDEFINED:
			default:
				resultOrientation = ORIENTATION_UNDEFINED;
				break;
		}
		setAttribute(TAG_ORIENTATION, Integer.toString(resultOrientation));
	}

	/**
	 * Returns if the current image orientation is flipped.
	 *
	 * @see #getRotationDegrees()
	 */
	public boolean isFlipped() {
		int orientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
		switch (orientation) {
			case ORIENTATION_FLIP_HORIZONTAL:
			case ORIENTATION_TRANSVERSE:
			case ORIENTATION_FLIP_VERTICAL:
			case ORIENTATION_TRANSPOSE:
				return true;
			default:
				return false;
		}
	}

	/**
	 * Returns the rotation degrees for the current image orientation. If the image is flipped,
	 * i.e., {@link #isFlipped()} returns {@code true}, the rotation degrees will be base on
	 * the assumption that the image is first flipped horizontally (along Y-axis), and then do
	 * the rotation. For example, {@link #ORIENTATION_TRANSPOSE} will be interpreted as flipped
	 * horizontally first, and then rotate 270 degrees clockwise.
	 *
	 * @return The rotation degrees of the image after the horizontal flipping is applied, if any.
	 *
	 * @see #isFlipped()
	 */
	public int getRotationDegrees() {
		int orientation = getAttributeInt(TAG_ORIENTATION, ORIENTATION_NORMAL);
		switch (orientation) {
			case ORIENTATION_ROTATE_90:
			case ORIENTATION_TRANSVERSE:
				return 90;
			case ORIENTATION_ROTATE_180:
			case ORIENTATION_FLIP_VERTICAL:
				return 180;
			case ORIENTATION_ROTATE_270:
			case ORIENTATION_TRANSPOSE:
				return 270;
			case ORIENTATION_UNDEFINED:
			case ORIENTATION_NORMAL:
			case ORIENTATION_FLIP_HORIZONTAL:
			default:
				return 0;
		}
	}

	/**
	 * Update the values of the tags in the tag groups if any value for the tag already was stored.
	 *
	 * @param tag the name of the tag.
	 * @param value the value of the tag in a form of {@link ExifAttribute}.
	 * @return Returns {@code true} if updating is placed.
	 */
	private boolean updateAttribute(String tag, ExifAttribute value) {
		boolean updated = false;
		for (int i = 0 ; i < EXIF_TAGS.length; ++i) {
			if (mAttributes[i].containsKey(tag)) {
				mAttributes[i].put(tag, value);
				updated = true;
			}
		}
		return updated;
	}

	/**
	 * Remove any values of the specified tag.
	 *
	 * @param tag the name of the tag.
	 */
	private void removeAttribute(String tag) {
		for (int i = 0 ; i < EXIF_TAGS.length; ++i) {
			mAttributes[i].remove(tag);
		}
	}

	/**
	 * This function decides which parser to read the image data according to the given input stream
	 * type and the content of the input stream. In each case, it reads the first three bytes to
	 * determine whether the image data format is JPEG or not.
	 */
	private void loadAttributes(@NonNull InputStream in) throws IOException {
		try {
			// Initialize mAttributes.
			for (int i = 0; i < EXIF_TAGS.length; ++i) {
				mAttributes[i] = new HashMap<>();
			}

			// Check file type
			in = new BufferedInputStream(in, SIGNATURE_CHECK_SIZE);
			mMimeType = getMimeType((BufferedInputStream) in);

			// Create byte-ordered input stream
			ByteOrderedDataInputStream inputStream = new ByteOrderedDataInputStream(in);

			switch (mMimeType) {
				case IMAGE_TYPE_JPEG: {
					getJpegAttributes(inputStream, 0, IFD_TYPE_PRIMARY); // 0 is offset
					break;
				}
				case IMAGE_TYPE_RAF: {
					getRafAttributes(inputStream);
					break;
				}
				case IMAGE_TYPE_HEIF: {
					getHeifAttributes(inputStream);
					break;
				}
				case IMAGE_TYPE_ORF: {
					getOrfAttributes(inputStream);
					break;
				}
				case IMAGE_TYPE_RW2: {
					getRw2Attributes(inputStream);
					break;
				}
				case IMAGE_TYPE_PNG: {
					getPngAttributes(inputStream);
					break;
				}
				case IMAGE_TYPE_WEBP: {
					getWebpAttributes(inputStream);
					break;
				}
				case IMAGE_TYPE_ARW:
				case IMAGE_TYPE_CR2:
				case IMAGE_TYPE_DNG:
				case IMAGE_TYPE_NEF:
				case IMAGE_TYPE_NRW:
				case IMAGE_TYPE_PEF:
				case IMAGE_TYPE_SRW:
				case IMAGE_TYPE_UNKNOWN: {
					getRawAttributes(inputStream);
					break;
				}
				default: {
					break;
				}
			}
			// Set thumbnail image offset and length
			setThumbnailData(inputStream);
			mIsSupportedFile = true;
		} catch (IOException e) {
			// Ignore exceptions in order to keep the compatibility with the old versions of
			// ExifInterface.
			mIsSupportedFile = false;
			if (DEBUG) {
				logger.warn("Invalid image: ExifInterface got an unsupported image format file"
						+ "(ExifInterface supports JPEG and some RAW image formats only) "
						+ "or a corrupted JPEG file to ExifInterface.", e);
			}
		} finally {
			addDefaultValuesForCompatibility();

			if (DEBUG) {
				printAttributes();
			}
		}
	}

	// Prints out attributes for debugging.
	private void printAttributes() {
		for (int i = 0; i < mAttributes.length; ++i) {
			logger.debug("The size of tag group[" + i + "]: " + mAttributes[i].size());
			for (Map.Entry<String, ExifAttribute> entry : mAttributes[i].entrySet()) {
				final ExifAttribute tagValue = entry.getValue();
				logger.debug("tagName: " + entry.getKey() + ", tagType: " + tagValue.toString()
						+ ", tagValue: '" + tagValue.getStringValue(mExifByteOrder) + "'");
			}
		}
	}

	/**
	 * Save the tag data into the original image file. This is expensive because it involves
	 * copying all the data from one file to another and deleting the old file and renaming the
	 * other. It's best to use {@link #setAttribute(String,String)} to set all attributes to write
	 * and make a single call rather than multiple calls for each attribute.
	 * <p>
	 * This method is only supported for JPEG files.
	 * </p>
	 */
	public void saveAttributes() throws IOException {
		if (!mIsSupportedFile || mMimeType != IMAGE_TYPE_JPEG) {
			throw new IOException("ExifInterface only supports saving attributes on JPEG formats.");
		}
		if (mFilename == null) {
			throw new IOException(
					"ExifInterface does not support saving attributes for the current input.");
		}

		// Keep the thumbnail in memory
		mThumbnailBytes = getThumbnail();

		File tempFile = new File(mFilename + ".tmp");
		File originalFile = new File(mFilename);
		if (!originalFile.renameTo(tempFile)) {
			throw new IOException("Could not rename to " + tempFile.getAbsolutePath());
		}

		FileInputStream in = null;
		FileOutputStream out = null;
		try {
			// Save the new file.
			in = new FileInputStream(tempFile);
			out = new FileOutputStream(mFilename);
			saveJpegAttributes(in, out, true);
		} finally {
			closeQuietly(in);
			closeQuietly(out);
			FileUtil.deleteFileOrWarn(tempFile, "saveAttributes", logger);
		}

		// Discard the thumbnail in memory
		mThumbnailBytes = null;
	}

	/**
	 * Save the tag data into the original image file. This is expensive because it involves
	 * copying all the data from one file to another and deleting the old file and renaming the
	 * other. It's best to use {@link #setAttribute(String,String)} to set all attributes to write
	 * and make a single call rather than multiple calls for each attribute.
	 * <p>
	 * This method is only supported for JPEG files.
	 * </p>
	 */
	public void saveAttributes(InputStream inputStream, OutputStream outputStream, boolean noExif) throws IOException {
		if (!mIsSupportedFile || mMimeType != IMAGE_TYPE_JPEG) {
			throw new IOException("ExifInterface only supports saving attributes on JPEG formats.");
		}

		// Keep the thumbnail in memory
		mThumbnailBytes = getThumbnail();

		saveJpegAttributes(inputStream, outputStream, noExif);

		// Discard the thumbnail in memory
		mThumbnailBytes = null;
	}

	/**
	 * Returns true if the image file has a thumbnail.
	 */
	public boolean hasThumbnail() {
		return mHasThumbnail;
	}

	/**
	 * Returns the JPEG compressed thumbnail inside the image file, or {@code null} if there is no
	 * JPEG compressed thumbnail.
	 * The returned data can be decoded using
	 * {@link android.graphics.BitmapFactory#decodeByteArray(byte[],int,int)}
	 */
	@Nullable
	public byte[] getThumbnail() {
		if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
			return getThumbnailBytes();
		}
		return null;
	}

	/**
	 * Returns the thumbnail bytes inside the image file, regardless of the compression type of the
	 * thumbnail image.
	 */
	@Nullable
	public byte[] getThumbnailBytes() {
		if (!mHasThumbnail) {
			return null;
		}
		if (mThumbnailBytes != null) {
			return mThumbnailBytes;
		}

		// Read the thumbnail.
		InputStream in = null;
		try {
			if (mAssetInputStream != null) {
				in = mAssetInputStream;
				if (in.markSupported()) {
					in.reset();
				} else {
					logger.debug("Cannot read thumbnail from inputstream without mark/reset support");
					return null;
				}
			} else if (mFilename != null) {
				in = new FileInputStream(mFilename);
			}
			if (in == null) {
				// Should not be reached this.
				throw new FileNotFoundException();
			}
			if (in.skip(mThumbnailOffset) != mThumbnailOffset) {
				throw new IOException("Corrupted image");
			}
			byte[] buffer = new byte[mThumbnailLength];
			if (in.read(buffer) != mThumbnailLength) {
				throw new IOException("Corrupted image");
			}
			mThumbnailBytes = buffer;
			return buffer;
		} catch (IOException e) {
			// Couldn't get a thumbnail image.
			logger.debug("Encountered exception while getting thumbnail", e);
		} finally {
			closeQuietly(in);
		}
		return null;
	}

	/**
	 * Creates and returns a Bitmap object of the thumbnail image based on the byte array and the
	 * thumbnail compression value, or {@code null} if the compression type is unsupported.
	 */
	@Nullable
	public Bitmap getThumbnailBitmap() {
		if (!mHasThumbnail) {
			return null;
		} else if (mThumbnailBytes == null) {
			mThumbnailBytes = getThumbnailBytes();
		}

		if (mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED) {
			return BitmapFactory.decodeByteArray(mThumbnailBytes, 0, mThumbnailLength);
		} else if (mThumbnailCompression == DATA_UNCOMPRESSED) {
			int[] rgbValues = new int[mThumbnailBytes.length / 3];
			byte alpha = (byte) 0xff000000;
			for (int i = 0; i < rgbValues.length; i++) {
				rgbValues[i] = alpha + (mThumbnailBytes[3 * i] << 16)
						+ (mThumbnailBytes[3 * i + 1] << 8) + mThumbnailBytes[3 * i + 2];
			}

			ExifAttribute imageLengthAttribute =
					(ExifAttribute) mAttributes[IFD_TYPE_THUMBNAIL].get(TAG_IMAGE_LENGTH);
			ExifAttribute imageWidthAttribute =
					(ExifAttribute) mAttributes[IFD_TYPE_THUMBNAIL].get(TAG_IMAGE_WIDTH);
			if (imageLengthAttribute != null && imageWidthAttribute != null) {
				int imageLength = imageLengthAttribute.getIntValue(mExifByteOrder);
				int imageWidth = imageWidthAttribute.getIntValue(mExifByteOrder);
				return Bitmap.createBitmap(
						rgbValues, imageWidth, imageLength, Bitmap.Config.ARGB_8888);
			}
		}
		return null;
	}

	/**
	 * Returns true if thumbnail image is JPEG Compressed, or false if either thumbnail image does
	 * not exist or thumbnail image is uncompressed.
	 */
	public boolean isThumbnailCompressed() {
		return mThumbnailCompression == DATA_JPEG || mThumbnailCompression == DATA_JPEG_COMPRESSED;
	}

	/**
	 * Returns the offset and length of thumbnail inside the image file, or
	 * {@code null} if there is no thumbnail.
	 *
	 * @return two-element array, the offset in the first value, and length in
	 *         the second, or {@code null} if no thumbnail was found.
	 */
	@Nullable
	public long[] getThumbnailRange() {
		if (!mHasThumbnail) {
			return null;
		}

		long[] range = new long[2];
		range[0] = mThumbnailOffset;
		range[1] = mThumbnailLength;

		return range;
	}

	/**
	 * Stores the latitude and longitude value in a float array. The first element is the latitude,
	 * and the second element is the longitude. Returns false if the Exif tags are not available.
	 *
	 * @deprecated Use {@link #getLatLong()} instead.
	 */
	@Deprecated
	public boolean getLatLong(float output[]) {
		double[] latLong = getLatLong();
		if (latLong == null) {
			return false;
		}

		output[0] = (float) latLong[0];
		output[1] = (float) latLong[1];
		return true;
	}

	/**
	 * Gets the latitude and longitude values.
	 * <p>
	 * If there are valid latitude and longitude values in the image, this method returns a double
	 * array where the first element is the latitude and the second element is the longitude.
	 * Otherwise, it returns null.
	 */
	@Nullable
	public double[] getLatLong() {
		String latValue = getAttribute(TAG_GPS_LATITUDE);
		String latRef = getAttribute(TAG_GPS_LATITUDE_REF);
		String lngValue = getAttribute(TAG_GPS_LONGITUDE);
		String lngRef = getAttribute(TAG_GPS_LONGITUDE_REF);

		if (latValue != null && latRef != null && lngValue != null && lngRef != null) {
			try {
				double latitude = convertRationalLatLonToDouble(latValue, latRef);
				double longitude = convertRationalLatLonToDouble(lngValue, lngRef);
				return new double[] {latitude, longitude};
			} catch (IllegalArgumentException e) {
				logger.warn("Latitude/longitude values are not parseable. " +
						String.format("latValue=%s, latRef=%s, lngValue=%s, lngRef=%s",
								latValue, latRef, lngValue, lngRef));
			}
		}
		return null;
	}

	/**
	 * Sets the GPS-related information. It will set GPS processing method, latitude and longitude
	 * values, GPS timestamp, and speed information at the same time.
	 *
	 * @param location the {@link Location} object returned by GPS service.
	 */
	public void setGpsInfo(Location location) {
		if (location == null) {
			return;
		}
		setAttribute(ExifInterface.TAG_GPS_PROCESSING_METHOD, location.getProvider());
		setLatLong(location.getLatitude(), location.getLongitude());
		setAltitude(location.getAltitude());
		// Location objects store speeds in m/sec. Translates it to km/hr here.
		setAttribute(TAG_GPS_SPEED_REF, "K");
		setAttribute(TAG_GPS_SPEED, new Rational(location.getSpeed()
				* TimeUnit.HOURS.toSeconds(1) / 1000).toString());
		String[] dateTime = sFormatter.format(new Date(location.getTime())).split("\\s+", -1);
		setAttribute(ExifInterface.TAG_GPS_DATESTAMP, dateTime[0]);
		setAttribute(ExifInterface.TAG_GPS_TIMESTAMP, dateTime[1]);
	}

	/**
	 * Sets the latitude and longitude values.
	 *
	 * @param latitude the decimal value of latitude. Must be a valid double value between -90.0 and
	 *                 90.0.
	 * @param longitude the decimal value of longitude. Must be a valid double value between -180.0
	 *                  and 180.0.
	 * @throws IllegalArgumentException If {@code latitude} or {@code longitude} is outside the
	 *                                  specified range.
	 */
	public void setLatLong(double latitude, double longitude) {
		if (latitude < -90.0 || latitude > 90.0 || Double.isNaN(latitude)) {
			throw new IllegalArgumentException("Latitude value " + latitude + " is not valid.");
		}
		if (longitude < -180.0 || longitude > 180.0 || Double.isNaN(longitude)) {
			throw new IllegalArgumentException("Longitude value " + longitude + " is not valid.");
		}
		setAttribute(TAG_GPS_LATITUDE_REF, latitude >= 0 ? "N" : "S");
		setAttribute(TAG_GPS_LATITUDE, convertDecimalDegree(Math.abs(latitude)));
		setAttribute(TAG_GPS_LONGITUDE_REF, longitude >= 0 ? "E" : "W");
		setAttribute(TAG_GPS_LONGITUDE, convertDecimalDegree(Math.abs(longitude)));
	}

	/**
	 * Return the altitude in meters. If the exif tag does not exist, return
	 * <var>defaultValue</var>.
	 *
	 * @param defaultValue the value to return if the tag is not available.
	 */
	public double getAltitude(double defaultValue) {
		double altitude = getAttributeDouble(TAG_GPS_ALTITUDE, -1);
		int ref = getAttributeInt(TAG_GPS_ALTITUDE_REF, -1);

		if (altitude >= 0 && ref >= 0) {
			return (altitude * ((ref == 1) ? -1 : 1));
		} else {
			return defaultValue;
		}
	}

	/**
	 * Sets the altitude in meters.
	 */
	public void setAltitude(double altitude) {
		String ref = altitude >= 0 ? "0" : "1";
		setAttribute(TAG_GPS_ALTITUDE, new Rational(Math.abs(altitude)).toString());
		setAttribute(TAG_GPS_ALTITUDE_REF, ref);
	}

	/**
	 * Set the date time value.
	 *
	 * @param timeStamp number of milliseconds since Jan. 1, 1970, midnight local time.
	 * @hide
	 */
	@RestrictTo(RestrictTo.Scope.LIBRARY)
	public void setDateTime(long timeStamp) {
		long sub = timeStamp % 1000;
		setAttribute(TAG_DATETIME, sFormatter.format(new Date(timeStamp)));
		setAttribute(TAG_SUBSEC_TIME, Long.toString(sub));
	}

	/**
	 * Returns number of milliseconds since Jan. 1, 1970, midnight local time.
	 * Returns -1 if the date time information if not available.
	 * @hide
	 */
	@RestrictTo(RestrictTo.Scope.LIBRARY)
	public long getDateTime() {
		String dateTimeString = getAttribute(TAG_DATETIME);
		if (dateTimeString == null
				|| !sNonZeroTimePattern.matcher(dateTimeString).matches()) return -1;

		ParsePosition pos = new ParsePosition(0);
		try {
			// The exif field is in local time. Parsing it as if it is UTC will yield time
			// since 1/1/1970 local time
			Date datetime = sFormatter.parse(dateTimeString, pos);
			if (datetime == null) return -1;
			long msecs = datetime.getTime();

			String subSecs = getAttribute(TAG_SUBSEC_TIME);
			if (subSecs != null) {
				try {
					long sub = Long.parseLong(subSecs);
					while (sub > 1000) {
						sub /= 10;
					}
					msecs += sub;
				} catch (NumberFormatException e) {
					// Ignored
				}
			}
			return msecs;
		} catch (IllegalArgumentException e) {
			return -1;
		}
	}

	/**
	 * Returns number of milliseconds since Jan. 1, 1970, midnight UTC.
	 * Returns -1 if the date time information if not available.
	 * @hide
	 */
	@RestrictTo(RestrictTo.Scope.LIBRARY)
	public long getGpsDateTime() {
		String date = getAttribute(TAG_GPS_DATESTAMP);
		String time = getAttribute(TAG_GPS_TIMESTAMP);
		if (date == null || time == null
				|| (!sNonZeroTimePattern.matcher(date).matches()
				&& !sNonZeroTimePattern.matcher(time).matches())) {
			return -1;
		}

		String dateTimeString = date + ' ' + time;

		ParsePosition pos = new ParsePosition(0);
		try {
			Date datetime = sFormatter.parse(dateTimeString, pos);
			if (datetime == null) return -1;
			return datetime.getTime();
		} catch (IllegalArgumentException e) {
			return -1;
		}
	}

	private static double convertRationalLatLonToDouble(String rationalString, String ref) {
		try {
			String [] parts = rationalString.split(",", -1);

			String [] pair;
			pair = parts[0].split("/", -1);
			double degrees = Double.parseDouble(pair[0].trim())
					/ Double.parseDouble(pair[1].trim());

			pair = parts[1].split("/", -1);
			double minutes = Double.parseDouble(pair[0].trim())
					/ Double.parseDouble(pair[1].trim());

			pair = parts[2].split("/", -1);
			double seconds = Double.parseDouble(pair[0].trim())
					/ Double.parseDouble(pair[1].trim());

			double result = degrees + (minutes / 60.0) + (seconds / 3600.0);
			if ((ref.equals("S") || ref.equals("W"))) {
				return -result;
			} else if (ref.equals("N") || ref.equals("E")) {
				return result;
			} else {
				// Not valid
				throw new IllegalArgumentException();
			}
		} catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
			// Not valid
			throw new IllegalArgumentException();
		}
	}

	private String convertDecimalDegree(double decimalDegree) {
		long degrees = (long) decimalDegree;
		long minutes = (long) ((decimalDegree - degrees) * 60.0);
		long seconds = Math.round((decimalDegree - degrees - minutes / 60.0) * 3600.0 * 1e7);
		return degrees + "/1," + minutes + "/1," + seconds + "/10000000";
	}

	// Checks the type of image file
	private int getMimeType(BufferedInputStream in) throws IOException {
		// TODO (b/142218289): Need to handle case where input stream does not support mark
		in.mark(SIGNATURE_CHECK_SIZE);
		byte[] signatureCheckBytes = new byte[SIGNATURE_CHECK_SIZE];
		in.read(signatureCheckBytes);
		in.reset();
		if (isJpegFormat(signatureCheckBytes)) {
			return IMAGE_TYPE_JPEG;
		} else if (isRafFormat(signatureCheckBytes)) {
			return IMAGE_TYPE_RAF;
		} else if (isHeifFormat(signatureCheckBytes)) {
			return IMAGE_TYPE_HEIF;
		} else if (isOrfFormat(signatureCheckBytes)) {
			return IMAGE_TYPE_ORF;
		} else if (isRw2Format(signatureCheckBytes)) {
			return IMAGE_TYPE_RW2;
		} else if (isPngFormat(signatureCheckBytes)) {
			return IMAGE_TYPE_PNG;
		} else if (isWebpFormat(signatureCheckBytes)) {
			return IMAGE_TYPE_WEBP;
		}
		// Certain file formats (PEF) are identified in readImageFileDirectory()
		return IMAGE_TYPE_UNKNOWN;
	}

	/**
	 * This method looks at the first 3 bytes to determine if this file is a JPEG file.
	 * See http://www.media.mit.edu/pia/Research/deepview/exif.html, "JPEG format and Marker"
	 */
	public static boolean isJpegFormat(byte[] signatureCheckBytes) throws IOException {
		for (int i = 0; i < JPEG_SIGNATURE.length; i++) {
			if (signatureCheckBytes[i] != JPEG_SIGNATURE[i]) {
				return false;
			}
		}
		return true;
	}

	/**
	 * This method looks at the first 15 bytes to determine if this file is a RAF file.
	 * There is no official specification for RAF files from Fuji, but there is an online archive of
	 * image file specifications:
	 * http://fileformats.archiveteam.org/wiki/Fujifilm_RAF
	 */
	private boolean isRafFormat(byte[] signatureCheckBytes) throws IOException {
		byte[] rafSignatureBytes = RAF_SIGNATURE.getBytes(Charset.defaultCharset());
		for (int i = 0; i < rafSignatureBytes.length; i++) {
			if (signatureCheckBytes[i] != rafSignatureBytes[i]) {
				return false;
			}
		}
		return true;
	}

	private boolean isHeifFormat(byte[] signatureCheckBytes) throws IOException {
		ByteOrderedDataInputStream signatureInputStream = null;
		try {
			signatureInputStream = new ByteOrderedDataInputStream(signatureCheckBytes);
			signatureInputStream.setByteOrder(ByteOrder.BIG_ENDIAN);

			long chunkSize = signatureInputStream.readInt();
			byte[] chunkType = new byte[4];
			signatureInputStream.read(chunkType);

			if (!Arrays.equals(chunkType, HEIF_TYPE_FTYP)) {
				return false;
			}

			long chunkDataOffset = 8;
			if (chunkSize == 1) {
				// This indicates that the next 8 bytes represent the chunk size,
				// and chunk data comes after that.
				chunkSize = signatureInputStream.readLong();
				if (chunkSize < 16) {
					// The smallest valid chunk is 16 bytes long in this case.
					return false;
				}
				chunkDataOffset += 8;
			}

			// only sniff up to signatureCheckBytes.length
			if (chunkSize > signatureCheckBytes.length) {
				chunkSize = signatureCheckBytes.length;
			}

			long chunkDataSize = chunkSize - chunkDataOffset;

			// It should at least have major brand (4-byte) and minor version (4-byte).
			// The rest of the chunk (if any) is a list of (4-byte) compatible brands.
			if (chunkDataSize < 8) {
				return false;
			}

			byte[] brand = new byte[4];
			boolean isMif1 = false;
			boolean isHeic = false;
			for (long i = 0; i < chunkDataSize / 4;  ++i) {
				if (signatureInputStream.read(brand) != brand.length) {
					return false;
				}
				if (i == 1) {
					// Skip this index, it refers to the minorVersion, not a brand.
					continue;
				}
				if (Arrays.equals(brand, HEIF_BRAND_MIF1)) {
					isMif1 = true;
				} else if (Arrays.equals(brand, HEIF_BRAND_HEIC)) {
					isHeic = true;
				}
				if (isMif1 && isHeic) {
					return true;
				}
			}
		} catch (Exception e) {
			if (DEBUG) {
				logger.debug("Exception parsing HEIF file type box.", e);
			}
		} finally {
			if (signatureInputStream != null) {
				signatureInputStream.close();
				signatureInputStream = null;
			}
		}
		return false;
	}

	/**
	 * ORF has a similar structure to TIFF but it contains a different signature at the TIFF Header.
	 * This method looks at the 2 bytes following the Byte Order bytes to determine if this file is
	 * an ORF file.
	 * There is no official specification for ORF files from Olympus, but there is an online archive
	 * of image file specifications:
	 * http://fileformats.archiveteam.org/wiki/Olympus_ORF
	 */
	private boolean isOrfFormat(byte[] signatureCheckBytes) throws IOException {
		ByteOrderedDataInputStream signatureInputStream =
				new ByteOrderedDataInputStream(signatureCheckBytes);
		// Read byte order
		mExifByteOrder = readByteOrder(signatureInputStream);
		// Set byte order
		signatureInputStream.setByteOrder(mExifByteOrder);

		short orfSignature = signatureInputStream.readShort();
		signatureInputStream.close();
		return orfSignature == ORF_SIGNATURE_1 || orfSignature == ORF_SIGNATURE_2;
	}

	/**
	 * RW2 is TIFF-based, but stores 0x55 signature byte instead of 0x42 at the header
	 * See http://lclevy.free.fr/raw/
	 */
	private boolean isRw2Format(byte[] signatureCheckBytes) throws IOException {
		ByteOrderedDataInputStream signatureInputStream =
				new ByteOrderedDataInputStream(signatureCheckBytes);
		// Read byte order
		mExifByteOrder = readByteOrder(signatureInputStream);
		// Set byte order
		signatureInputStream.setByteOrder(mExifByteOrder);

		short signatureByte = signatureInputStream.readShort();
		signatureInputStream.close();
		return signatureByte == RW2_SIGNATURE;
	}


	/**
	 * PNG's file signature is first 8 bytes.
	 * See PNG (Portable Network Graphics) Specification, Version 1.2, 3.1. PNG file signature
	 */
	private boolean isPngFormat(byte[] signatureCheckBytes) throws IOException {
		for (int i = 0; i < PNG_SIGNATURE.length; i++) {
			if (signatureCheckBytes[i] != PNG_SIGNATURE[i]) {
				return false;
			}
		}
		return true;
	}

	/**
	 * WebP's file signature is composed of 12 bytes:
	 *   'RIFF' (4 bytes) + file length value (4 bytes) + 'WEBP' (4 bytes)
	 * See https://developers.google.com/speed/webp/docs/riff_container, Section "WebP File Header"
	 */
	private boolean isWebpFormat(byte[] signatureCheckBytes) throws IOException {
		for (int i = 0; i < WEBP_SIGNATURE_1.length; i++) {
			if (signatureCheckBytes[i] != WEBP_SIGNATURE_1[i]) {
				return false;
			}
		}
		for (int i = 0; i < WEBP_SIGNATURE_2.length; i++) {
			if (signatureCheckBytes[i + WEBP_SIGNATURE_1.length + WEBP_FILE_SIZE_BYTE_LENGTH]
					!= WEBP_SIGNATURE_2[i]) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Loads EXIF attributes from a JPEG input stream.
	 *
	 * @param in The input stream that starts with the JPEG data.
	 * @param jpegOffset The offset value in input stream for JPEG data.
	 * @param imageType The image type from which to retrieve metadata. Use IFD_TYPE_PRIMARY for
	 *                   primary image, IFD_TYPE_PREVIEW for preview image, and
	 *                   IFD_TYPE_THUMBNAIL for thumbnail image.
	 * @throws IOException If the data contains invalid JPEG markers, offsets, or length values.
	 */
	private void getJpegAttributes(ByteOrderedDataInputStream in, int jpegOffset, int imageType)
			throws IOException {
		// See JPEG File Interchange Format Specification, "JFIF Specification"
		if (DEBUG) {
			logger.debug("getJpegAttributes starting with: " + in);
		}

		// JPEG uses Big Endian by default. See https://people.cs.umass.edu/~verts/cs32/endian.html
		in.setByteOrder(ByteOrder.BIG_ENDIAN);

		// Skip to JPEG data
		in.seek(jpegOffset);
		int bytesRead = jpegOffset;

		byte marker;
		if ((marker = in.readByte()) != MARKER) {
			throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
		}
		++bytesRead;
		if (in.readByte() != MARKER_SOI) {
			throw new IOException("Invalid marker: " + Integer.toHexString(marker & 0xff));
		}
		++bytesRead;
		while (true) {
			marker = in.readByte();
			if (marker != MARKER) {
				throw new IOException("Invalid marker:" + Integer.toHexString(marker & 0xff));
			}
			++bytesRead;
			marker = in.readByte();
			if (DEBUG) {
				logger.debug("Found JPEG segment indicator: " + Integer.toHexString(marker & 0xff));
			}
			++bytesRead;

			// EOI indicates the end of an image and in case of SOS, JPEG image stream starts and
			// the image data will terminate right after.
			if (marker == MARKER_EOI || marker == MARKER_SOS) {
				break;
			}
			int length = in.readUnsignedShort() - 2;
			bytesRead += 2;
			if (DEBUG) {
				logger.debug("JPEG segment: " + Integer.toHexString(marker & 0xff) + " (length: "
						+ (length + 2) + ")");
			}
			if (length < 0) {
				throw new IOException("Invalid length");
			}
			switch (marker) {
				case MARKER_APP1: {
					if (DEBUG) {
						logger.debug("MARKER_APP1");
					}
					if (length < 6) {
						// Skip if it's not an EXIF APP1 segment.
						break;
					}
					byte[] identifier = new byte[6];
					if (in.read(identifier) != 6) {
						throw new IOException("Invalid exif");
					}
					bytesRead += 6;
					length -= 6;
					if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
						// Skip if it's not an EXIF APP1 segment.
						break;
					}
					if (length <= 0) {
						throw new IOException("Invalid exif");
					}
					if (DEBUG) {
						logger.debug("readExifSegment with a byte array (length: " + length + ")");
					}
					// Save offset values for createJpegThumbnailBitmap() function
					mExifOffset = bytesRead;

					byte[] bytes = new byte[length];
					if (in.read(bytes) != length) {
						throw new IOException("Invalid exif");
					}
					bytesRead += length;
					length = 0;

					readExifSegment(bytes, imageType);
					break;
				}

				case MARKER_COM: {
					byte[] bytes = new byte[length];
					if (in.read(bytes) != length) {
						throw new IOException("Invalid exif");
					}
					length = 0;
					if (getAttribute(TAG_USER_COMMENT) == null) {
						mAttributes[IFD_TYPE_EXIF].put(TAG_USER_COMMENT, ExifAttribute.createString(
								new String(bytes, US_ASCII)));
					}
					break;
				}

				case MARKER_SOF0:
				case MARKER_SOF1:
				case MARKER_SOF2:
				case MARKER_SOF3:
				case MARKER_SOF5:
				case MARKER_SOF6:
				case MARKER_SOF7:
				case MARKER_SOF9:
				case MARKER_SOF10:
				case MARKER_SOF11:
				case MARKER_SOF13:
				case MARKER_SOF14:
				case MARKER_SOF15: {
					if (in.skipBytes(1) != 1) {
						throw new IOException("Invalid SOFx");
					}
					mAttributes[imageType].put(TAG_IMAGE_LENGTH, ExifAttribute.createULong(
							in.readUnsignedShort(), mExifByteOrder));
					mAttributes[imageType].put(TAG_IMAGE_WIDTH, ExifAttribute.createULong(
							in.readUnsignedShort(), mExifByteOrder));
					length -= 5;
					break;
				}

				default: {
					break;
				}
			}
			if (length < 0) {
				throw new IOException("Invalid length");
			}
			if (in.skipBytes(length) != length) {
				throw new IOException("Invalid JPEG segment");
			}
			bytesRead += length;
		}
		// Restore original byte order
		in.setByteOrder(mExifByteOrder);
	}

	private void getRawAttributes(ByteOrderedDataInputStream in) throws IOException {
		// Parse TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
		parseTiffHeaders(in, in.available());

		// Read TIFF image file directories. See JEITA CP-3451C Section 4.5.2. Figure 6.
		readImageFileDirectory(in, IFD_TYPE_PRIMARY);

		// Update ImageLength/Width tags for all image data.
		updateImageSizeValues(in, IFD_TYPE_PRIMARY);
		updateImageSizeValues(in, IFD_TYPE_PREVIEW);
		updateImageSizeValues(in, IFD_TYPE_THUMBNAIL);

		// Check if each image data is in valid position.
		validateImages();

		if (mMimeType == IMAGE_TYPE_PEF) {
			// PEF files contain a MakerNote data, which contains the data for ColorSpace tag.
			// See http://lclevy.free.fr/raw/ and piex.cc PefGetPreviewData()
			ExifAttribute makerNoteAttribute =
					(ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_MAKER_NOTE);
			if (makerNoteAttribute != null) {
				// Create an ordered DataInputStream for MakerNote

				// MODIFIED BY THREEMA
				try (ByteOrderedDataInputStream makerNoteDataInputStream =
						new ByteOrderedDataInputStream(makerNoteAttribute.bytes)) {
					makerNoteDataInputStream.setByteOrder(mExifByteOrder);

					// Seek to MakerNote data
					makerNoteDataInputStream.seek(PEF_MAKER_NOTE_SKIP_SIZE);

					// Read IFD data from MakerNote
					readImageFileDirectory(makerNoteDataInputStream, IFD_TYPE_PEF);

					// Update ColorSpace tag
					ExifAttribute colorSpaceAttribute =
						(ExifAttribute) mAttributes[IFD_TYPE_PEF].get(TAG_COLOR_SPACE);
					if (colorSpaceAttribute != null) {
						mAttributes[IFD_TYPE_EXIF].put(TAG_COLOR_SPACE, colorSpaceAttribute);
					}
				}
			}
		}
	}

	/**
	 * RAF files contains a JPEG and a CFA data.
	 * The JPEG contains two images, a preview and a thumbnail, while the CFA contains a RAW image.
	 * This method looks at the first 160 bytes of a RAF file to retrieve the offset and length
	 * values for the JPEG and CFA data.
	 * Using that data, it parses the JPEG data to retrieve the preview and thumbnail image data,
	 * then parses the CFA metadata to retrieve the primary image length/width values.
	 * For data format details, see http://fileformats.archiveteam.org/wiki/Fujifilm_RAF
	 */
	private void getRafAttributes(ByteOrderedDataInputStream in) throws IOException {
		// Retrieve offset & length values
		in.skipBytes(RAF_OFFSET_TO_JPEG_IMAGE_OFFSET);
		byte[] jpegOffsetBytes = new byte[4];
		byte[] cfaHeaderOffsetBytes = new byte[4];
		in.read(jpegOffsetBytes);
		// Skip JPEG length value since it is not needed
		in.skipBytes(RAF_JPEG_LENGTH_VALUE_SIZE);
		in.read(cfaHeaderOffsetBytes);
		int rafJpegOffset = ByteBuffer.wrap(jpegOffsetBytes).getInt();
		int rafCfaHeaderOffset = ByteBuffer.wrap(cfaHeaderOffsetBytes).getInt();

		// Retrieve JPEG image metadata
		getJpegAttributes(in, rafJpegOffset, IFD_TYPE_PREVIEW);

		// Skip to CFA header offset.
		in.seek(rafCfaHeaderOffset);

		// Retrieve primary image length/width values, if TAG_RAF_IMAGE_SIZE exists
		in.setByteOrder(ByteOrder.BIG_ENDIAN);
		int numberOfDirectoryEntry = in.readInt();
		if (DEBUG) {
			logger.debug("numberOfDirectoryEntry: " + numberOfDirectoryEntry);
		}
		// CFA stores some metadata about the RAW image. Since CFA uses proprietary tags, can only
		// find and retrieve image size information tags, while skipping others.
		// See piex.cc RafGetDimension()
		for (int i = 0; i < numberOfDirectoryEntry; ++i) {
			int tagNumber = in.readUnsignedShort();
			int numberOfBytes = in.readUnsignedShort();
			if (tagNumber == TAG_RAF_IMAGE_SIZE.number) {
				int imageLength = in.readShort();
				int imageWidth = in.readShort();
				ExifAttribute imageLengthAttribute =
						ExifAttribute.createUShort(imageLength, mExifByteOrder);
				ExifAttribute imageWidthAttribute =
						ExifAttribute.createUShort(imageWidth, mExifByteOrder);
				mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, imageLengthAttribute);
				mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, imageWidthAttribute);
				if (DEBUG) {
					logger.debug("Updated to length: " + imageLength + ", width: " + imageWidth);
				}
				return;
			}
			in.skipBytes(numberOfBytes);
		}
	}

	@TargetApi(Build.VERSION_CODES.P)
	private void getHeifAttributes(final ByteOrderedDataInputStream in) throws IOException {
		// MODIFIED BY THREEMA
		// do not use automatic resource management on MediaMetadataRetriever
		final MediaMetadataRetriever retriever = new MediaMetadataRetriever();
		try {
			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
				retriever.setDataSource(new MediaDataSource() {
					long mPosition;

					@Override
					public void close() throws IOException {}

					@Override
					public int readAt(long position, byte[] buffer, int offset, int size)
							throws IOException {
						if (size == 0) {
							return 0;
						}
						if (position < 0) {
							return -1;
						}
						try {
							if (mPosition != position) {
								// We don't allow seek to positions after the available bytes,
								// the input stream won't be able to seek back then.
								// However, if we hit an exception before (mPosition set to -1),
								// let it try the seek in hope it might recover.
								if (mPosition >= 0 && position >= mPosition + in.available()) {
									return -1;
								}
								in.seek(position);
								mPosition = position;
							}

							// If the read will cause us to go over the available bytes,
							// reduce the size so that we stay in the available range.
							// Otherwise the input stream may not be able to seek back.
							if (size > in.available()) {
								size = in.available();
							}

							int bytesRead = in.read(buffer, offset, size);
							if (bytesRead >= 0) {
								mPosition += bytesRead;
								return bytesRead;
							}
						} catch (IOException e) {
							// do nothing
						}
						mPosition = -1; // need to seek on next read
						return -1;
					}

					@Override
					public long getSize() throws IOException {
						return -1;
					}
				});
			} else {
				if (mFilename != null) {
					retriever.setDataSource(mFilename);
				} else {
					return;
				}
			}

			String exifOffsetStr = retriever.extractMetadata(
					33); // MediaMetadataRetriever.METADATA_KEY_EXIF_OFFSET
			String exifLengthStr = retriever.extractMetadata(
					34); // MediaMetadataRetriever.METADATA_KEY_EXIF_LENGTH
			String hasImage = retriever.extractMetadata(
						MediaMetadataRetriever.METADATA_KEY_HAS_IMAGE);
			String hasVideo = retriever.extractMetadata(
					MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);

			String width = null;
			String height = null;
			String rotation = null;
			final String metadataValueYes = "yes";
			// If the file has both image and video, prefer image info over video info.
			// App querying ExifInterface is most likely using the bitmap path which
			// picks the image first.
			if (metadataValueYes.equals(hasImage)) {
				width = retriever.extractMetadata(
						MediaMetadataRetriever.METADATA_KEY_IMAGE_WIDTH);
				height = retriever.extractMetadata(
						MediaMetadataRetriever.METADATA_KEY_IMAGE_HEIGHT);
				rotation = retriever.extractMetadata(
						MediaMetadataRetriever.METADATA_KEY_IMAGE_ROTATION);
			} else if (metadataValueYes.equals(hasVideo)) {
				width = retriever.extractMetadata(
						MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
				height = retriever.extractMetadata(
						MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
				rotation = retriever.extractMetadata(
						MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
			}

			if (width != null) {
				mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH,
						ExifAttribute.createUShort(Integer.parseInt(width), mExifByteOrder));
			}

			if (height != null) {
				mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH,
						ExifAttribute.createUShort(Integer.parseInt(height), mExifByteOrder));
			}

			if (rotation != null) {
				int orientation = ExifInterface.ORIENTATION_NORMAL;

				// all rotation angles in CW
				switch (Integer.parseInt(rotation)) {
					case 90:
						orientation = ORIENTATION_ROTATE_90;
						break;
					case 180:
						orientation = ORIENTATION_ROTATE_180;
						break;
					case 270:
						orientation = ORIENTATION_ROTATE_270;
						break;
				}

				mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION,
						ExifAttribute.createUShort(orientation, mExifByteOrder));
			}

			if (exifOffsetStr != null && exifLengthStr != null) {
				int offset = Integer.parseInt(exifOffsetStr);
				int length = Integer.parseInt(exifLengthStr);
				if (length <= 6) {
					throw new IOException("Invalid exif length");
				}
				in.seek(offset);
				byte[] identifier = new byte[6];
				if (in.read(identifier) != 6) {
					throw new IOException("Can't read identifier");
				}
				offset += 6;
				length -= 6;
				if (!Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
					throw new IOException("Invalid identifier");
				}

				// TODO: Need to handle potential OutOfMemoryError
				byte[] bytes = new byte[length];
				if (in.read(bytes) != length) {
					throw new IOException("Can't read exif");
				}
				readExifSegment(bytes, IFD_TYPE_PRIMARY);
			}

			if (DEBUG) {
				logger.debug("Heif meta: " + width + "x" + height + ", rotation " + rotation);
			}
		} finally {
			retriever.release();
		}
	}

	/**
	 * ORF files contains a primary image data and a MakerNote data that contains preview/thumbnail
	 * images. Both data takes the form of IFDs and can therefore be read with the
	 * readImageFileDirectory() method.
	 * This method reads all the necessary data and updates the primary/preview/thumbnail image
	 * information according to the GetOlympusPreviewImage() method in piex.cc.
	 * For data format details, see the following:
	 * http://fileformats.archiveteam.org/wiki/Olympus_ORF
	 * https://libopenraw.freedesktop.org/wiki/Olympus_ORF
	 */
	private void getOrfAttributes(ByteOrderedDataInputStream in) throws IOException {
		// Retrieve primary image data
		// Other Exif data will be located in the Makernote.
		getRawAttributes(in);

		// Additionally retrieve preview/thumbnail information from MakerNote tag, which contains
		// proprietary tags and therefore does not have offical documentation
		// See GetOlympusPreviewImage() in piex.cc & http://www.exiv2.org/tags-olympus.html
		ExifAttribute makerNoteAttribute =
				(ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_MAKER_NOTE);
		if (makerNoteAttribute != null) {
			// Create an ordered DataInputStream for MakerNote
			// MODIFIED BY THREEMA
			try (ByteOrderedDataInputStream makerNoteDataInputStream =
					new ByteOrderedDataInputStream(makerNoteAttribute.bytes)) {
				makerNoteDataInputStream.setByteOrder(mExifByteOrder);

				// There are two types of headers for Olympus MakerNotes
				// See http://www.exiv2.org/makernote.html#R1
				byte[] makerNoteHeader1Bytes = new byte[ORF_MAKER_NOTE_HEADER_1.length];
				makerNoteDataInputStream.readFully(makerNoteHeader1Bytes);
				makerNoteDataInputStream.seek(0);
				byte[] makerNoteHeader2Bytes = new byte[ORF_MAKER_NOTE_HEADER_2.length];
				makerNoteDataInputStream.readFully(makerNoteHeader2Bytes);
				// Skip the corresponding amount of bytes for each header type
				if (Arrays.equals(makerNoteHeader1Bytes, ORF_MAKER_NOTE_HEADER_1)) {
					makerNoteDataInputStream.seek(ORF_MAKER_NOTE_HEADER_1_SIZE);
				} else if (Arrays.equals(makerNoteHeader2Bytes, ORF_MAKER_NOTE_HEADER_2)) {
					makerNoteDataInputStream.seek(ORF_MAKER_NOTE_HEADER_2_SIZE);
				}

				// Read IFD data from MakerNote
				readImageFileDirectory(makerNoteDataInputStream, IFD_TYPE_ORF_MAKER_NOTE);
			}

			// Retrieve & update preview image offset & length values
			ExifAttribute imageStartAttribute = (ExifAttribute)
					mAttributes[IFD_TYPE_ORF_CAMERA_SETTINGS].get(TAG_ORF_PREVIEW_IMAGE_START);
			ExifAttribute imageLengthAttribute = (ExifAttribute)
					mAttributes[IFD_TYPE_ORF_CAMERA_SETTINGS].get(TAG_ORF_PREVIEW_IMAGE_LENGTH);

			if (imageStartAttribute != null && imageLengthAttribute != null) {
				mAttributes[IFD_TYPE_PREVIEW].put(TAG_JPEG_INTERCHANGE_FORMAT,
						imageStartAttribute);
				mAttributes[IFD_TYPE_PREVIEW].put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
						imageLengthAttribute);
			}

			// TODO: Check this behavior in other ORF files
			// Retrieve primary image length & width values
			// See piex.cc GetOlympusPreviewImage()
			ExifAttribute aspectFrameAttribute = (ExifAttribute)
					mAttributes[IFD_TYPE_ORF_IMAGE_PROCESSING].get(TAG_ORF_ASPECT_FRAME);
			if (aspectFrameAttribute != null) {
				int[] aspectFrameValues = (int[]) aspectFrameAttribute.getValue(mExifByteOrder);
				if (aspectFrameValues == null || aspectFrameValues.length != 4) {
					logger.warn("Invalid aspect frame values. frame="
							+ Arrays.toString(aspectFrameValues));
					return;
				}
				if (aspectFrameValues[2] > aspectFrameValues[0] &&
						aspectFrameValues[3] > aspectFrameValues[1]) {
					int primaryImageWidth = aspectFrameValues[2] - aspectFrameValues[0] + 1;
					int primaryImageLength = aspectFrameValues[3] - aspectFrameValues[1] + 1;
					// Swap width & length values
					if (primaryImageWidth < primaryImageLength) {
						primaryImageWidth += primaryImageLength;
						primaryImageLength = primaryImageWidth - primaryImageLength;
						primaryImageWidth -= primaryImageLength;
					}
					ExifAttribute primaryImageWidthAttribute =
							ExifAttribute.createUShort(primaryImageWidth, mExifByteOrder);
					ExifAttribute primaryImageLengthAttribute =
							ExifAttribute.createUShort(primaryImageLength, mExifByteOrder);

					mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, primaryImageWidthAttribute);
					mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, primaryImageLengthAttribute);
				}
			}
		}
	}

	// RW2 contains the primary image data in IFD0 and the preview and/or thumbnail image data in
	// the JpgFromRaw tag
	// See https://libopenraw.freedesktop.org/wiki/Panasonic_RAW/ and piex.cc Rw2GetPreviewData()
	private void getRw2Attributes(ByteOrderedDataInputStream in) throws IOException {
		// Retrieve primary image data
		getRawAttributes(in);

		// Retrieve preview and/or thumbnail image data
		ExifAttribute jpgFromRawAttribute =
				(ExifAttribute) mAttributes[IFD_TYPE_PRIMARY].get(TAG_RW2_JPG_FROM_RAW);
		if (jpgFromRawAttribute != null) {
			getJpegAttributes(in, mRw2JpgFromRawOffset, IFD_TYPE_PREVIEW);
		}

		// Set ISO tag value if necessary
		ExifAttribute rw2IsoAttribute =
				(ExifAttribute) mAttributes[IFD_TYPE_PRIMARY].get(TAG_RW2_ISO);
		ExifAttribute exifIsoAttribute =
				(ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_PHOTOGRAPHIC_SENSITIVITY);
		if (rw2IsoAttribute != null && exifIsoAttribute == null) {
			// Place this attribute only if it doesn't exist
			mAttributes[IFD_TYPE_EXIF].put(TAG_PHOTOGRAPHIC_SENSITIVITY, rw2IsoAttribute);
		}
	}

	// PNG contains the EXIF data as a Special-Purpose Chunk
	private void getPngAttributes(ByteOrderedDataInputStream in) throws IOException {
		if (DEBUG) {
			logger.debug("getPngAttributes starting with: " + in);
		}

		// PNG uses Big Endian by default.
		// See PNG (Portable Network Graphics) Specification, Version 1.2,
		// 2.1. Integers and byte order
		in.setByteOrder(ByteOrder.BIG_ENDIAN);

		int bytesRead = 0;

		// Skip the signature bytes
		in.skipBytes(PNG_SIGNATURE.length);
		bytesRead += PNG_SIGNATURE.length;

		// Each chunk is made up of four parts:
		//   1) Length: 4-byte unsigned integer indicating the number of bytes in the
		//   Chunk Data field. Excludes Chunk Type and CRC bytes.
		//   2) Chunk Type: 4-byte chunk type code.
		//   3) Chunk Data: The data bytes. Can be zero-length.
		//   4) CRC: 4-byte data calculated on the preceding bytes in the chunk. Always
		//   present.
		// --> 4 (length bytes) + 4 (type bytes) + X (data bytes) + 4 (CRC bytes)
		// See PNG (Portable Network Graphics) Specification, Version 1.2,
		// 3.2. Chunk layout
		try {
			while (true) {
				int length = in.readInt();
				bytesRead += 4;

				byte[] type = new byte[PNG_CHUNK_TYPE_BYTE_LENGTH];
				if (in.read(type) != type.length) {
					throw new IOException("Encountered invalid length while parsing PNG chunk"
							+ "type");
				}
				bytesRead += PNG_CHUNK_TYPE_BYTE_LENGTH;

				// The first chunk must be the IHDR chunk
				if (bytesRead == 16 && !Arrays.equals(type, PNG_CHUNK_TYPE_IHDR)) {
					throw new IOException("Encountered invalid PNG file--IHDR chunk should appear"
							+ "as the first chunk");
				}

				if (Arrays.equals(type, PNG_CHUNK_TYPE_IEND)) {
					// IEND marks the end of the image.
					break;
				} else if (Arrays.equals(type, PNG_CHUNK_TYPE_EXIF)) {
					// TODO: Need to handle potential OutOfMemoryError
					byte[] data = new byte[length];
					if (in.read(data) != length) {
						throw new IOException("Failed to read given length for given PNG chunk "
								+ "type: " + byteArrayToHexString(type));
					}

					// Compare CRC values for potential data corruption.
					int dataCrcValue = in.readInt();
					// Cyclic Redundancy Code used to check for corruption of the data
					CRC32 crc = new CRC32();
					crc.update(type);
					crc.update(data);
					if ((int) crc.getValue() != dataCrcValue) {
						throw new IOException("Encountered invalid CRC value for PNG-EXIF chunk."
								+ "\n recorded CRC value: " + dataCrcValue + ", calculated CRC "
								+ "value: " + crc.getValue());
					}

					readExifSegment(data, IFD_TYPE_PRIMARY);

					validateImages();

					// Save offset values for handleThumbnailFromJfif() function
					mExifOffset = bytesRead;
					break;
				} else {
					// Skip to next chunk
					in.skipBytes(length + PNG_CHUNK_CRC_BYTE_LENGTH);
					bytesRead += length + PNG_CHUNK_CRC_BYTE_LENGTH;
				}
			}
		} catch (EOFException e) {
			// Should not reach here. Will only reach here if the file is corrupted or
			// does not follow the PNG specifications
			throw new IOException("Encountered corrupt PNG file.");
		}
	}

	// WebP contains EXIF data as a RIFF File Format Chunk
	// All references below can be found in the following link.
	// https://developers.google.com/speed/webp/docs/riff_container
	private void getWebpAttributes(ByteOrderedDataInputStream in) throws IOException {
		if (DEBUG) {
			logger.debug("getWebpAttributes starting with: " + in);
		}

		// WebP uses little-endian by default.
		// See Section "Terminology & Basics"
		in.setByteOrder(ByteOrder.LITTLE_ENDIAN);

		in.skipBytes(WEBP_SIGNATURE_1.length);
		// File size corresponds to the size of the entire file from offset 8.
		// See Section "WebP File Header"
		int fileSize = in.readInt() + 8;
		int bytesRead = 8;
		bytesRead += in.skipBytes(WEBP_SIGNATURE_2.length);

		try {
			while (true) {
				// Each chunk is made up of three parts:
				//   1) Chunk FourCC: 4-byte concatenating four ASCII characters.
				//   2) Chunk Size: 4-byte unsigned integer indicating the size of the chunk.
				//                  Excludes Chunk FourCC and Chunk Size bytes.
				//   3) Chunk Payload: data payload. A single padding byte ('0') is added if
				//                     Chunk Size is odd.
				// See Section "RIFF File Format"
				byte[] code = new byte[WEBP_CHUNK_TYPE_BYTE_LENGTH];
				if (in.read(code) != code.length) {
					throw new IOException("Encountered invalid length while parsing WebP chunk"
							+ "type");
				}
				bytesRead += 4;

				int chunkSize = in.readInt();
				bytesRead += 4;

				if (Arrays.equals(WEBP_CHUNK_TYPE_EXIF, code)) {
					// TODO: Need to handle potential OutOfMemoryError
					byte[] payload = new byte[chunkSize];
					if (in.read(payload) != chunkSize) {
						throw new IOException("Failed to read given length for given PNG chunk "
								+ "type: " + byteArrayToHexString(code));
					}
					readExifSegment(payload, IFD_TYPE_PRIMARY);
					break;
				} else {
					// Add a single padding byte at end if chunk size is odd
					chunkSize = (chunkSize % 2 == 1) ? chunkSize + 1 : chunkSize;

					// Check if skipping to next chunk is necessary
					if (bytesRead + chunkSize == fileSize) {
						// Reached end of file
						break;
					} else if (bytesRead + chunkSize > fileSize) {
						throw new IOException("Encountered WebP file with invalid chunk size");
					}

					// Skip to next chunk
					int skipped = in.skipBytes(chunkSize);
					if (skipped != chunkSize) {
						throw new IOException("Encountered WebP file with invalid chunk size");
					}
					bytesRead += skipped;
				}
			}
			// Save offset values for handleThumbnailFromJfif() function
			mExifOffset = bytesRead;
		} catch (EOFException e) {
			// Should not reach here. Will only reach here if the file is corrupted or
			// does not follow the WebP specifications
			throw new IOException("Encountered corrupt WebP file.");
		}
	}

	// Stores a new JPEG image with EXIF attributes into a given output stream.
	private void saveJpegAttributes(InputStream inputStream, OutputStream outputStream, boolean stripExif)
			throws IOException {
		// See JPEG File Interchange Format Specification, "JFIF Specification"
		if (DEBUG) {
			logger.debug("saveJpegAttributes starting with (inputStream: " + inputStream
					+ ", outputStream: " + outputStream + ")");
		}
		DataInputStream dataInputStream = new DataInputStream(inputStream);
		ByteOrderedDataOutputStream dataOutputStream =
				new ByteOrderedDataOutputStream(outputStream, ByteOrder.BIG_ENDIAN);
		if (dataInputStream.readByte() != MARKER) {
			throw new IOException("Invalid marker");
		}
		dataOutputStream.writeByte(MARKER);
		if (dataInputStream.readByte() != MARKER_SOI) {
			throw new IOException("Invalid marker");
		}
		dataOutputStream.writeByte(MARKER_SOI);

		// Write EXIF APP1 segment
		if (!stripExif) {
			dataOutputStream.writeByte(MARKER);
			dataOutputStream.writeByte(MARKER_APP1);
			writeExifSegment(dataOutputStream, 6);
		}

		byte[] bytes = new byte[4096];

		while (true) {
			byte marker = dataInputStream.readByte();
			if (marker != MARKER) {
				throw new IOException("Invalid marker");
			}
			marker = dataInputStream.readByte();
			switch (marker) {
				case MARKER_APP1: {
					int length = dataInputStream.readUnsignedShort() - 2;
					if (length < 0) {
						throw new IOException("Invalid length");
					}
					byte[] identifier = new byte[6];
					if (length >= 6) {
						if (dataInputStream.read(identifier) != 6) {
							throw new IOException("Invalid exif");
						}
						if (Arrays.equals(identifier, IDENTIFIER_EXIF_APP1)) {
							// Skip the original EXIF APP1 segment.
							if (dataInputStream.skipBytes(length - 6) != length - 6) {
								throw new IOException("Invalid length");
							}
							break;
						}
					}
					// Copy non-EXIF APP1 segment.
					dataOutputStream.writeByte(MARKER);
					dataOutputStream.writeByte(marker);
					dataOutputStream.writeUnsignedShort(length + 2);
					if (length >= 6) {
						length -= 6;
						dataOutputStream.write(identifier);
					}
					int read;
					while (length > 0 && (read = dataInputStream.read(
							bytes, 0, Math.min(length, bytes.length))) >= 0) {
						dataOutputStream.write(bytes, 0, read);
						length -= read;
					}
					break;
				}
				case MARKER_EOI:
				case MARKER_SOS: {
					dataOutputStream.writeByte(MARKER);
					dataOutputStream.writeByte(marker);
					// Copy all the remaining data
					copy(dataInputStream, dataOutputStream);
					return;
				}
				default: {
					// Copy JPEG segment
					dataOutputStream.writeByte(MARKER);
					dataOutputStream.writeByte(marker);
					int length = dataInputStream.readUnsignedShort();
					dataOutputStream.writeUnsignedShort(length);
					length -= 2;
					if (length < 0) {
						throw new IOException("Invalid length");
					}
					int read;
					while (length > 0 && (read = dataInputStream.read(
							bytes, 0, Math.min(length, bytes.length))) >= 0) {
						dataOutputStream.write(bytes, 0, read);
						length -= read;
					}
					break;
				}
			}
		}
	}

	// Reads the given EXIF byte area and save its tag data into attributes.
	private void readExifSegment(byte[] exifBytes, int imageType) throws IOException {
		ByteOrderedDataInputStream dataInputStream =
				new ByteOrderedDataInputStream(exifBytes);

		// Parse TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
		parseTiffHeaders(dataInputStream, exifBytes.length);

		// Read TIFF image file directories. See JEITA CP-3451C Section 4.5.2. Figure 6.
		readImageFileDirectory(dataInputStream, imageType);
	}

	private void addDefaultValuesForCompatibility() {
		// If DATETIME tag has no value, then set the value to DATETIME_ORIGINAL tag's.
		String valueOfDateTimeOriginal = getAttribute(TAG_DATETIME_ORIGINAL);
		if (valueOfDateTimeOriginal != null && getAttribute(TAG_DATETIME) == null) {
			mAttributes[IFD_TYPE_PRIMARY].put(TAG_DATETIME,
					ExifAttribute.createString(valueOfDateTimeOriginal));
		}

		// Add the default value.
		if (getAttribute(TAG_IMAGE_WIDTH) == null) {
			mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH,
					ExifAttribute.createULong(0, mExifByteOrder));
		}
		if (getAttribute(TAG_IMAGE_LENGTH) == null) {
			mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH,
					ExifAttribute.createULong(0, mExifByteOrder));
		}
		if (getAttribute(TAG_ORIENTATION) == null) {
			mAttributes[IFD_TYPE_PRIMARY].put(TAG_ORIENTATION,
					ExifAttribute.createULong(0, mExifByteOrder));
		}
		if (getAttribute(TAG_LIGHT_SOURCE) == null) {
			mAttributes[IFD_TYPE_EXIF].put(TAG_LIGHT_SOURCE,
					ExifAttribute.createULong(0, mExifByteOrder));
		}
	}

	private ByteOrder readByteOrder(ByteOrderedDataInputStream dataInputStream)
			throws IOException {
		// Read byte order.
		short byteOrder = dataInputStream.readShort();
		switch (byteOrder) {
			case BYTE_ALIGN_II:
				if (DEBUG) {
					logger.debug("readExifSegment: Byte Align II");
				}
				return ByteOrder.LITTLE_ENDIAN;
			case BYTE_ALIGN_MM:
				if (DEBUG) {
					logger.debug("readExifSegment: Byte Align MM");
				}
				return ByteOrder.BIG_ENDIAN;
			default:
				throw new IOException("Invalid byte order: " + Integer.toHexString(byteOrder));
		}
	}

	private void parseTiffHeaders(ByteOrderedDataInputStream dataInputStream,
	                              int exifBytesLength) throws IOException {
		// Read byte order
		mExifByteOrder = readByteOrder(dataInputStream);
		// Set byte order
		dataInputStream.setByteOrder(mExifByteOrder);

		// Check start code
		int startCode = dataInputStream.readUnsignedShort();
		if (mMimeType != IMAGE_TYPE_ORF && mMimeType != IMAGE_TYPE_RW2 && startCode != START_CODE) {
			throw new IOException("Invalid start code: " + Integer.toHexString(startCode));
		}

		// Read and skip to first ifd offset
		int firstIfdOffset = dataInputStream.readInt();
		if (firstIfdOffset < 8 || firstIfdOffset >= exifBytesLength) {
			throw new IOException("Invalid first Ifd offset: " + firstIfdOffset);
		}
		firstIfdOffset -= 8;
		if (firstIfdOffset > 0) {
			if (dataInputStream.skipBytes(firstIfdOffset) != firstIfdOffset) {
				throw new IOException("Couldn't jump to first Ifd: " + firstIfdOffset);
			}
		}
	}

	// Reads image file directory, which is a tag group in EXIF.
	private void readImageFileDirectory(ByteOrderedDataInputStream dataInputStream,
	                                    @IfdType int ifdType) throws IOException {
		// Save offset of current IFD to prevent reading an IFD that is already read.
		mAttributesOffsets.add(dataInputStream.mPosition);

		if (dataInputStream.mPosition + 2 > dataInputStream.mLength) {
			// Return if there is no data from the offset.
			return;
		}
		// See TIFF 6.0 Section 2: TIFF Structure, Figure 1.
		short numberOfDirectoryEntry = dataInputStream.readShort();
		if (DEBUG) {
			logger.debug("numberOfDirectoryEntry: " + numberOfDirectoryEntry);
		}
		if (dataInputStream.mPosition + 12 * numberOfDirectoryEntry > dataInputStream.mLength
				|| numberOfDirectoryEntry <= 0) {
			// Return if the size of entries is either too big or negative.
			return;
		}

		// See TIFF 6.0 Section 2: TIFF Structure, "Image File Directory".
		for (short i = 0; i < numberOfDirectoryEntry; ++i) {
			int tagNumber = dataInputStream.readUnsignedShort();
			int dataFormat = dataInputStream.readUnsignedShort();
			int numberOfComponents = dataInputStream.readInt();
			// Next four bytes is for data offset or value.
			long nextEntryOffset = dataInputStream.peek() + 4L;

			// Look up a corresponding tag from tag number
			ExifTag tag = (ExifTag) sExifTagMapsForReading[ifdType].get(tagNumber);

			if (DEBUG) {
				logger.debug(String.format("ifdType: %d, tagNumber: %d, tagName: %s, dataFormat: %d, "
								+ "numberOfComponents: %d", ifdType, tagNumber,
						tag != null ? tag.name : null, dataFormat, numberOfComponents));
			}

			long byteCount = 0;
			boolean valid = false;
			if (tag == null) {
				logger.warn("Skip the tag entry since tag number is not defined: " + tagNumber);
			} else if (dataFormat <= 0 || dataFormat >= IFD_FORMAT_BYTES_PER_FORMAT.length) {
				logger.warn("Skip the tag entry since data format is invalid: " + dataFormat);
			} else if (!tag.isFormatCompatible(dataFormat)) {
				logger.warn("Skip the tag entry since data format is unexpected for tag: " + tag.name);
			} else {
				if (dataFormat == IFD_FORMAT_UNDEFINED) {
					dataFormat = tag.primaryFormat;
				}
				byteCount = (long) numberOfComponents * IFD_FORMAT_BYTES_PER_FORMAT[dataFormat];
				if (byteCount < 0 || byteCount > Integer.MAX_VALUE) {
					logger.warn("Skip the tag entry since the number of components is invalid: "
							+ numberOfComponents);
				} else {
					valid = true;
				}
			}
			if (!valid) {
				dataInputStream.seek(nextEntryOffset);
				continue;
			}

			// Read a value from data field or seek to the value offset which is stored in data
			// field if the size of the entry value is bigger than 4.
			if (byteCount > 4) {
				int offset = dataInputStream.readInt();
				if (DEBUG) {
					logger.debug("seek to data offset: " + offset);
				}
				if (mMimeType == IMAGE_TYPE_ORF) {
					if (TAG_MAKER_NOTE.equals(tag.name)) {
						// Save offset value for reading thumbnail
						mOrfMakerNoteOffset = offset;
					} else if (ifdType == IFD_TYPE_ORF_MAKER_NOTE
							&& TAG_ORF_THUMBNAIL_IMAGE.equals(tag.name)) {
						// Retrieve & update values for thumbnail offset and length values for ORF
						mOrfThumbnailOffset = offset;
						mOrfThumbnailLength = numberOfComponents;

						ExifAttribute compressionAttribute =
								ExifAttribute.createUShort(DATA_JPEG, mExifByteOrder);
						ExifAttribute jpegInterchangeFormatAttribute =
								ExifAttribute.createULong(mOrfThumbnailOffset, mExifByteOrder);
						ExifAttribute jpegInterchangeFormatLengthAttribute =
								ExifAttribute.createULong(mOrfThumbnailLength, mExifByteOrder);

						mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_COMPRESSION, compressionAttribute);
						mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT,
								jpegInterchangeFormatAttribute);
						mAttributes[IFD_TYPE_THUMBNAIL].put(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
								jpegInterchangeFormatLengthAttribute);
					}
				} else if (mMimeType == IMAGE_TYPE_RW2) {
					if (TAG_RW2_JPG_FROM_RAW.equals(tag.name)) {
						mRw2JpgFromRawOffset = offset;
					}
				}
				if (offset + byteCount <= dataInputStream.mLength) {
					dataInputStream.seek(offset);
				} else {
					// Skip if invalid data offset.
					logger.warn("Skip the tag entry since data offset is invalid: " + offset);
					dataInputStream.seek(nextEntryOffset);
					continue;
				}
			}

			// Recursively parse IFD when a IFD pointer tag appears.
			Integer nextIfdType = sExifPointerTagMap.get(tagNumber);
			if (DEBUG) {
				logger.debug("nextIfdType: " + nextIfdType + " byteCount: " + byteCount);
			}

			if (nextIfdType != null) {
				long offset = -1L;
				// Get offset from data field
				switch (dataFormat) {
					case IFD_FORMAT_USHORT: {
						offset = dataInputStream.readUnsignedShort();
						break;
					}
					case IFD_FORMAT_SSHORT: {
						offset = dataInputStream.readShort();
						break;
					}
					case IFD_FORMAT_ULONG: {
						offset = dataInputStream.readUnsignedInt();
						break;
					}
					case IFD_FORMAT_SLONG:
					case IFD_FORMAT_IFD: {
						offset = dataInputStream.readInt();
						break;
					}
					default: {
						// Nothing to do
						break;
					}
				}
				if (DEBUG) {
					logger.debug(String.format("Offset: %d, tagName: %s", offset, tag.name));
				}

				// Check if the next IFD offset
				// 1. Exists within the boundaries of the input stream
				// 2. Does not point to a previously read IFD.
				if (offset > 0L && offset < dataInputStream.mLength) {
					if (!mAttributesOffsets.contains((int) offset)) {
						dataInputStream.seek(offset);
						readImageFileDirectory(dataInputStream, nextIfdType);
					} else {
						logger.warn("Skip jump into the IFD since it has already been read: "
								+ "IfdType " + nextIfdType + " (at " + offset + ")");
					}
				} else {
					logger.warn("Skip jump into the IFD since its offset is invalid: " + offset);
				}

				dataInputStream.seek(nextEntryOffset);
				continue;
			}

			byte[] bytes = new byte[(int) byteCount];
			dataInputStream.readFully(bytes);
			ExifAttribute attribute = new ExifAttribute(dataFormat, numberOfComponents, bytes);
			mAttributes[ifdType].put(tag.name, attribute);

			// DNG files have a DNG Version tag specifying the version of specifications that the
			// image file is following.
			// See http://fileformats.archiveteam.org/wiki/DNG
			if (TAG_DNG_VERSION.equals(tag.name)) {
				mMimeType = IMAGE_TYPE_DNG;
			}

			// PEF files have a Make or Model tag that begins with "PENTAX" or a compression tag
			// that is 65535.
			// See http://fileformats.archiveteam.org/wiki/Pentax_PEF
			if (((TAG_MAKE.equals(tag.name) || TAG_MODEL.equals(tag.name))
					&& attribute.getStringValue(mExifByteOrder).contains(PEF_SIGNATURE))
					|| (TAG_COMPRESSION.equals(tag.name)
					&& attribute.getIntValue(mExifByteOrder) == 65535)) {
				mMimeType = IMAGE_TYPE_PEF;
			}

			// Seek to next tag offset
			if (dataInputStream.peek() != nextEntryOffset) {
				dataInputStream.seek(nextEntryOffset);
			}
		}

		if (dataInputStream.peek() + 4 <= dataInputStream.mLength) {
			int nextIfdOffset = dataInputStream.readInt();
			if (DEBUG) {
				logger.debug(String.format("nextIfdOffset: %d", nextIfdOffset));
			}
			// Check if the next IFD offset
			// 1. Exists within the boundaries of the input stream
			// 2. Does not point to a previously read IFD.
			if (nextIfdOffset > 0L && nextIfdOffset < dataInputStream.mLength) {
				if (!mAttributesOffsets.contains(nextIfdOffset)) {
					dataInputStream.seek(nextIfdOffset);
					if (mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) {
						// Do not overwrite thumbnail IFD data if it alreay exists.
						readImageFileDirectory(dataInputStream, IFD_TYPE_THUMBNAIL);
					} else if (mAttributes[IFD_TYPE_PREVIEW].isEmpty()) {
						readImageFileDirectory(dataInputStream, IFD_TYPE_PREVIEW);
					}
				} else {
					logger.warn("Stop reading file since re-reading an IFD may cause an "
							+ "infinite loop: " + nextIfdOffset);
				}
			} else {
				logger.warn("Stop reading file since a wrong offset may cause an infinite loop: "
						+ nextIfdOffset);
			}
		}
	}

	/**
	 * JPEG compressed images do not contain IMAGE_LENGTH & IMAGE_WIDTH tags.
	 * This value uses JpegInterchangeFormat(JPEG data offset) value, and calls getJpegAttributes()
	 * to locate SOF(Start of Frame) marker and update the image length & width values.
	 * See JEITA CP-3451C Table 5 and Section 4.8.1. B.
	 */
	private void retrieveJpegImageSize(ByteOrderedDataInputStream in, int imageType)
			throws IOException {
		// Check if image already has IMAGE_LENGTH & IMAGE_WIDTH values
		ExifAttribute imageLengthAttribute =
				(ExifAttribute) mAttributes[imageType].get(TAG_IMAGE_LENGTH);
		ExifAttribute imageWidthAttribute =
				(ExifAttribute) mAttributes[imageType].get(TAG_IMAGE_WIDTH);

		if (imageLengthAttribute == null || imageWidthAttribute == null) {
			// Find if offset for JPEG data exists
			ExifAttribute jpegInterchangeFormatAttribute =
					(ExifAttribute) mAttributes[imageType].get(TAG_JPEG_INTERCHANGE_FORMAT);
			if (jpegInterchangeFormatAttribute != null) {
				int jpegInterchangeFormat =
						jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);

				// Searches for SOF marker in JPEG data and updates IMAGE_LENGTH & IMAGE_WIDTH tags
				getJpegAttributes(in, jpegInterchangeFormat, imageType);
			}
		}
	}

	// Sets thumbnail offset & length attributes based on JpegInterchangeFormat or StripOffsets tags
	private void setThumbnailData(ByteOrderedDataInputStream in) throws IOException {
		HashMap thumbnailData = mAttributes[IFD_TYPE_THUMBNAIL];

		ExifAttribute compressionAttribute =
				(ExifAttribute) thumbnailData.get(TAG_COMPRESSION);
		if (compressionAttribute != null) {
			mThumbnailCompression = compressionAttribute.getIntValue(mExifByteOrder);
			switch (mThumbnailCompression) {
				case DATA_JPEG: {
					handleThumbnailFromJfif(in, thumbnailData);
					break;
				}
				case DATA_UNCOMPRESSED:
				case DATA_JPEG_COMPRESSED: {
					if (isSupportedDataType(thumbnailData)) {
						handleThumbnailFromStrips(in, thumbnailData);
					}
					break;
				}
			}
		} else {
			// Thumbnail data may not contain Compression tag value
			mThumbnailCompression = DATA_JPEG;
			handleThumbnailFromJfif(in, thumbnailData);
		}
	}

	// Check JpegInterchangeFormat(JFIF) tags to retrieve thumbnail offset & length values
	// and reads the corresponding bytes if stream does not support seek function
	private void handleThumbnailFromJfif(ByteOrderedDataInputStream in, HashMap thumbnailData)
			throws IOException {
		ExifAttribute jpegInterchangeFormatAttribute =
				(ExifAttribute) thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT);
		ExifAttribute jpegInterchangeFormatLengthAttribute =
				(ExifAttribute) thumbnailData.get(TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
		if (jpegInterchangeFormatAttribute != null
				&& jpegInterchangeFormatLengthAttribute != null) {
			int thumbnailOffset = jpegInterchangeFormatAttribute.getIntValue(mExifByteOrder);
			int thumbnailLength = jpegInterchangeFormatLengthAttribute.getIntValue(mExifByteOrder);

			// The following code limits the size of thumbnail size not to overflow EXIF data area.
			thumbnailLength = Math.min(thumbnailLength, in.available() - thumbnailOffset);
			if (mMimeType == IMAGE_TYPE_JPEG || mMimeType == IMAGE_TYPE_RAF
					|| mMimeType == IMAGE_TYPE_RW2) {
				thumbnailOffset += mExifOffset;
			} else if (mMimeType == IMAGE_TYPE_ORF) {
				// Update offset value since RAF files have IFD data preceding MakerNote data.
				thumbnailOffset += mOrfMakerNoteOffset;
			}
			if (DEBUG) {
				logger.debug("Setting thumbnail attributes with offset: " + thumbnailOffset
						+ ", length: " + thumbnailLength);
			}
			if (thumbnailOffset > 0 && thumbnailLength > 0) {
				mHasThumbnail = true;
				mThumbnailOffset = thumbnailOffset;
				mThumbnailLength = thumbnailLength;
				if (mFilename == null && mAssetInputStream == null) {
					// Save the thumbnail in memory if the input doesn't support reading again.
					byte[] thumbnailBytes = new byte[thumbnailLength];
					in.seek(thumbnailOffset);
					in.readFully(thumbnailBytes);
					mThumbnailBytes = thumbnailBytes;
				}
			}
		}
	}

	// Check StripOffsets & StripByteCounts tags to retrieve thumbnail offset & length values
	private void handleThumbnailFromStrips(ByteOrderedDataInputStream in, HashMap thumbnailData)
			throws IOException {
		ExifAttribute stripOffsetsAttribute =
				(ExifAttribute) thumbnailData.get(TAG_STRIP_OFFSETS);
		ExifAttribute stripByteCountsAttribute =
				(ExifAttribute) thumbnailData.get(TAG_STRIP_BYTE_COUNTS);

		if (stripOffsetsAttribute != null && stripByteCountsAttribute != null) {
			long[] stripOffsets =
					convertToLongArray(stripOffsetsAttribute.getValue(mExifByteOrder));
			long[] stripByteCounts =
					convertToLongArray(stripByteCountsAttribute.getValue(mExifByteOrder));

			if (stripOffsets == null) {
				logger.warn("stripOffsets should not be null.");
				return;
			}
			if (stripByteCounts == null) {
				logger.warn("stripByteCounts should not be null.");
				return;
			}

			long totalStripByteCount = 0;
			for (long byteCount : stripByteCounts) {
				totalStripByteCount += byteCount;
			}

			// Set thumbnail byte array data for non-consecutive strip bytes
			byte[] totalStripBytes = new byte[(int) totalStripByteCount];

			int bytesRead = 0;
			int bytesAdded = 0;
			for (int i = 0; i < stripOffsets.length; i++) {
				int stripOffset = (int) stripOffsets[i];
				int stripByteCount = (int) stripByteCounts[i];

				// Skip to offset
				int skipBytes = stripOffset - bytesRead;
				if (skipBytes < 0) {
					logger.debug("Invalid strip offset value");
				}
				in.seek(skipBytes);
				bytesRead += skipBytes;

				// Read strip bytes
				byte[] stripBytes = new byte[stripByteCount];
				in.read(stripBytes);
				bytesRead += stripByteCount;

				// Add bytes to array
				System.arraycopy(stripBytes, 0, totalStripBytes, bytesAdded,
						stripBytes.length);
				bytesAdded += stripBytes.length;
			}

			mHasThumbnail = true;
			mThumbnailBytes = totalStripBytes;
			mThumbnailLength = totalStripBytes.length;
		}
	}

	// Check if thumbnail data type is currently supported or not
	private boolean isSupportedDataType(HashMap thumbnailData) throws IOException {
		ExifAttribute bitsPerSampleAttribute =
				(ExifAttribute) thumbnailData.get(TAG_BITS_PER_SAMPLE);
		if (bitsPerSampleAttribute != null) {
			int[] bitsPerSampleValue = (int[]) bitsPerSampleAttribute.getValue(mExifByteOrder);

			if (Arrays.equals(BITS_PER_SAMPLE_RGB, bitsPerSampleValue)) {
				return true;
			}

			// See DNG Specification 1.4.0.0. Section 3, Compression.
			if (mMimeType == IMAGE_TYPE_DNG) {
				ExifAttribute photometricInterpretationAttribute =
						(ExifAttribute) thumbnailData.get(TAG_PHOTOMETRIC_INTERPRETATION);
				if (photometricInterpretationAttribute != null) {
					int photometricInterpretationValue
							= photometricInterpretationAttribute.getIntValue(mExifByteOrder);
					if ((photometricInterpretationValue == PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO
							&& Arrays.equals(bitsPerSampleValue, BITS_PER_SAMPLE_GREYSCALE_2))
							|| ((photometricInterpretationValue == PHOTOMETRIC_INTERPRETATION_YCBCR)
							&& (Arrays.equals(bitsPerSampleValue, BITS_PER_SAMPLE_RGB)))) {
						return true;
					} else {
						// TODO: Add support for lossless Huffman JPEG data
					}
				}
			}
		}
		if (DEBUG) {
			logger.debug("Unsupported data type value");
		}
		return false;
	}

	// Returns true if the image length and width values are <= 512.
	// See Section 4.8 of http://standardsproposals.bsigroup.com/Home/getPDF/567
	private boolean isThumbnail(HashMap map) throws IOException {
		ExifAttribute imageLengthAttribute = (ExifAttribute) map.get(TAG_IMAGE_LENGTH);
		ExifAttribute imageWidthAttribute = (ExifAttribute) map.get(TAG_IMAGE_WIDTH);

		if (imageLengthAttribute != null && imageWidthAttribute != null) {
			int imageLengthValue = imageLengthAttribute.getIntValue(mExifByteOrder);
			int imageWidthValue = imageWidthAttribute.getIntValue(mExifByteOrder);
			if (imageLengthValue <= MAX_THUMBNAIL_SIZE && imageWidthValue <= MAX_THUMBNAIL_SIZE) {
				return true;
			}
		}
		return false;
	}

	// Validate primary, preview, thumbnail image data by comparing image size
	private void validateImages() throws IOException {
		// Swap images based on size (primary > preview > thumbnail)
		swapBasedOnImageSize(IFD_TYPE_PRIMARY, IFD_TYPE_PREVIEW);
		swapBasedOnImageSize(IFD_TYPE_PRIMARY, IFD_TYPE_THUMBNAIL);
		swapBasedOnImageSize(IFD_TYPE_PREVIEW, IFD_TYPE_THUMBNAIL);

		// Check if image has PixelXDimension/PixelYDimension tags, which contain valid image
		// sizes, excluding padding at the right end or bottom end of the image to make sure that
		// the values are multiples of 64. See JEITA CP-3451C Table 5 and Section 4.8.1. B.
		ExifAttribute pixelXDimAttribute =
				(ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_PIXEL_X_DIMENSION);
		ExifAttribute pixelYDimAttribute =
				(ExifAttribute) mAttributes[IFD_TYPE_EXIF].get(TAG_PIXEL_Y_DIMENSION);
		if (pixelXDimAttribute != null && pixelYDimAttribute != null) {
			mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_WIDTH, pixelXDimAttribute);
			mAttributes[IFD_TYPE_PRIMARY].put(TAG_IMAGE_LENGTH, pixelYDimAttribute);
		}

		// Check whether thumbnail image exists and whether preview image satisfies the thumbnail
		// image requirements
		if (mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) {
			if (isThumbnail(mAttributes[IFD_TYPE_PREVIEW])) {
				mAttributes[IFD_TYPE_THUMBNAIL] = mAttributes[IFD_TYPE_PREVIEW];
				mAttributes[IFD_TYPE_PREVIEW] = new HashMap<>();
			}
		}

		// Check if the thumbnail image satisfies the thumbnail size requirements
		if (!isThumbnail(mAttributes[IFD_TYPE_THUMBNAIL])) {
			logger.debug("No image meets the size requirements of a thumbnail image.");
		}
	}

	/**
	 * If image is uncompressed, ImageWidth/Length tags are used to store size info.
	 * However, uncompressed images often store extra pixels around the edges of the final image,
	 * which results in larger values for TAG_IMAGE_WIDTH and TAG_IMAGE_LENGTH tags.
	 * This method corrects those tag values by checking first the values of TAG_DEFAULT_CROP_SIZE
	 * See DNG Specification 1.4.0.0. Section 4. (DefaultCropSize)
	 *
	 * If image is a RW2 file, valid image sizes are stored in SensorBorder tags.
	 * See tiff_parser.cc GetFullDimension32()
	 * */
	private void updateImageSizeValues(ByteOrderedDataInputStream in, int imageType)
			throws IOException {
		// Uncompressed image valid image size values
		ExifAttribute defaultCropSizeAttribute =
				(ExifAttribute) mAttributes[imageType].get(TAG_DEFAULT_CROP_SIZE);
		// RW2 image valid image size values
		ExifAttribute topBorderAttribute =
				(ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_TOP_BORDER);
		ExifAttribute leftBorderAttribute =
				(ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_LEFT_BORDER);
		ExifAttribute bottomBorderAttribute =
				(ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_BOTTOM_BORDER);
		ExifAttribute rightBorderAttribute =
				(ExifAttribute) mAttributes[imageType].get(TAG_RW2_SENSOR_RIGHT_BORDER);

		if (defaultCropSizeAttribute != null) {
			// Update for uncompressed image
			ExifAttribute defaultCropSizeXAttribute, defaultCropSizeYAttribute;
			if (defaultCropSizeAttribute.format == IFD_FORMAT_URATIONAL) {
				Rational[] defaultCropSizeValue =
						(Rational[]) defaultCropSizeAttribute.getValue(mExifByteOrder);
				if (defaultCropSizeValue == null || defaultCropSizeValue.length != 2) {
					logger.warn("Invalid crop size values. cropSize="
							+ Arrays.toString(defaultCropSizeValue));
					return;
				}
				defaultCropSizeXAttribute =
						ExifAttribute.createURational(defaultCropSizeValue[0], mExifByteOrder);
				defaultCropSizeYAttribute =
						ExifAttribute.createURational(defaultCropSizeValue[1], mExifByteOrder);
			} else {
				int[] defaultCropSizeValue =
						(int[]) defaultCropSizeAttribute.getValue(mExifByteOrder);
				if (defaultCropSizeValue == null || defaultCropSizeValue.length != 2) {
					logger.warn("Invalid crop size values. cropSize="
							+ Arrays.toString(defaultCropSizeValue));
					return;
				}
				defaultCropSizeXAttribute =
						ExifAttribute.createUShort(defaultCropSizeValue[0], mExifByteOrder);
				defaultCropSizeYAttribute =
						ExifAttribute.createUShort(defaultCropSizeValue[1], mExifByteOrder);
			}
			mAttributes[imageType].put(TAG_IMAGE_WIDTH, defaultCropSizeXAttribute);
			mAttributes[imageType].put(TAG_IMAGE_LENGTH, defaultCropSizeYAttribute);
		} else if (topBorderAttribute != null && leftBorderAttribute != null &&
				bottomBorderAttribute != null && rightBorderAttribute != null) {
			// Update for RW2 image
			int topBorderValue = topBorderAttribute.getIntValue(mExifByteOrder);
			int bottomBorderValue = bottomBorderAttribute.getIntValue(mExifByteOrder);
			int rightBorderValue = rightBorderAttribute.getIntValue(mExifByteOrder);
			int leftBorderValue = leftBorderAttribute.getIntValue(mExifByteOrder);
			if (bottomBorderValue > topBorderValue && rightBorderValue > leftBorderValue) {
				int length = bottomBorderValue - topBorderValue;
				int width = rightBorderValue - leftBorderValue;
				ExifAttribute imageLengthAttribute =
						ExifAttribute.createUShort(length, mExifByteOrder);
				ExifAttribute imageWidthAttribute =
						ExifAttribute.createUShort(width, mExifByteOrder);
				mAttributes[imageType].put(TAG_IMAGE_LENGTH, imageLengthAttribute);
				mAttributes[imageType].put(TAG_IMAGE_WIDTH, imageWidthAttribute);
			}
		} else {
			retrieveJpegImageSize(in, imageType);
		}
	}

	// Writes an Exif segment into the given output stream.
	private int writeExifSegment(ByteOrderedDataOutputStream dataOutputStream,
	                             int exifOffsetFromBeginning) throws IOException {
		// The following variables are for calculating each IFD tag group size in bytes.
		int[] ifdOffsets = new int[EXIF_TAGS.length];
		int[] ifdDataSizes = new int[EXIF_TAGS.length];

		// Remove IFD pointer tags (we'll re-add it later.)
		for (ExifTag tag : EXIF_POINTER_TAGS) {
			removeAttribute(tag.name);
		}
		// Remove old thumbnail data
		removeAttribute(JPEG_INTERCHANGE_FORMAT_TAG.name);
		removeAttribute(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name);

		// Remove null value tags.
		for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
			for (Object obj : mAttributes[ifdType].entrySet().toArray()) {
				final Map.Entry entry = (Map.Entry) obj;
				if (entry.getValue() == null) {
					mAttributes[ifdType].remove(entry.getKey());
				}
			}
		}

		// Add IFD pointer tags. The next offset of primary image TIFF IFD will have thumbnail IFD
		// offset when there is one or more tags in the thumbnail IFD.
		if (!mAttributes[IFD_TYPE_EXIF].isEmpty()) {
			mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[1].name,
					ExifAttribute.createULong(0, mExifByteOrder));
		}
		if (!mAttributes[IFD_TYPE_GPS].isEmpty()) {
			mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[2].name,
					ExifAttribute.createULong(0, mExifByteOrder));
		}
		if (!mAttributes[IFD_TYPE_INTEROPERABILITY].isEmpty()) {
			mAttributes[IFD_TYPE_EXIF].put(EXIF_POINTER_TAGS[3].name,
					ExifAttribute.createULong(0, mExifByteOrder));
		}
		if (mHasThumbnail) {
			mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_TAG.name,
					ExifAttribute.createULong(0, mExifByteOrder));
			mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_LENGTH_TAG.name,
					ExifAttribute.createULong(mThumbnailLength, mExifByteOrder));
		}

		// Calculate IFD group data area sizes. IFD group data area is assigned to save the entry
		// value which has a bigger size than 4 bytes.
		for (int i = 0; i < EXIF_TAGS.length; ++i) {
			int sum = 0;
			for (Map.Entry<String, ExifAttribute> entry : mAttributes[i].entrySet()) {
				final ExifAttribute exifAttribute = entry.getValue();
				final int size = exifAttribute.size();
				if (size > 4) {
					sum += size;
				}
			}
			ifdDataSizes[i] += sum;
		}

		// Calculate IFD offsets.
		int position = 8;
		for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
			if (!mAttributes[ifdType].isEmpty()) {
				ifdOffsets[ifdType] = position;
				position += 2 + mAttributes[ifdType].size() * 12 + 4 + ifdDataSizes[ifdType];
			}
		}
		if (mHasThumbnail) {
			int thumbnailOffset = position;
			mAttributes[IFD_TYPE_THUMBNAIL].put(JPEG_INTERCHANGE_FORMAT_TAG.name,
					ExifAttribute.createULong(thumbnailOffset, mExifByteOrder));
			mThumbnailOffset = exifOffsetFromBeginning + thumbnailOffset;
			position += mThumbnailLength;
		}

		// Calculate the total size
		int totalSize = position + 8;  // eight bytes is for header part.
		if (DEBUG) {
			logger.debug("totalSize length: " + totalSize);
			for (int i = 0; i < EXIF_TAGS.length; ++i) {
				logger.debug(String.format("index: %d, offsets: %d, tag count: %d, data sizes: %d",
						i, ifdOffsets[i], mAttributes[i].size(), ifdDataSizes[i]));
			}
		}

		// Update IFD pointer tags with the calculated offsets.
		if (!mAttributes[IFD_TYPE_EXIF].isEmpty()) {
			mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[1].name,
					ExifAttribute.createULong(ifdOffsets[IFD_TYPE_EXIF], mExifByteOrder));
		}
		if (!mAttributes[IFD_TYPE_GPS].isEmpty()) {
			mAttributes[IFD_TYPE_PRIMARY].put(EXIF_POINTER_TAGS[2].name,
					ExifAttribute.createULong(ifdOffsets[IFD_TYPE_GPS], mExifByteOrder));
		}
		if (!mAttributes[IFD_TYPE_INTEROPERABILITY].isEmpty()) {
			mAttributes[IFD_TYPE_EXIF].put(EXIF_POINTER_TAGS[3].name, ExifAttribute.createULong(
					ifdOffsets[IFD_TYPE_INTEROPERABILITY], mExifByteOrder));
		}

		// Write TIFF Headers. See JEITA CP-3451C Section 4.5.2. Table 1.
		dataOutputStream.writeUnsignedShort(totalSize);
		dataOutputStream.write(IDENTIFIER_EXIF_APP1);
		dataOutputStream.writeShort(mExifByteOrder == ByteOrder.BIG_ENDIAN
				? BYTE_ALIGN_MM : BYTE_ALIGN_II);
		dataOutputStream.setByteOrder(mExifByteOrder);
		dataOutputStream.writeUnsignedShort(START_CODE);
		dataOutputStream.writeUnsignedInt(IFD_OFFSET);

		// Write IFD groups. See JEITA CP-3451C Section 4.5.8. Figure 9.
		for (int ifdType = 0; ifdType < EXIF_TAGS.length; ++ifdType) {
			if (!mAttributes[ifdType].isEmpty()) {
				// See JEITA CP-3451C Section 4.6.2: IFD structure.
				// Write entry count
				dataOutputStream.writeUnsignedShort(mAttributes[ifdType].size());

				// Write entry info
				int dataOffset = ifdOffsets[ifdType] + 2 + mAttributes[ifdType].size() * 12 + 4;
				for (Map.Entry<String, ExifAttribute> entry : mAttributes[ifdType].entrySet()) {
					// Convert tag name to tag number.
					final ExifTag tag = sExifTagMapsForWriting[ifdType].get(entry.getKey());
					final int tagNumber = tag.number;
					final ExifAttribute attribute = entry.getValue();
					final int size = attribute.size();

					dataOutputStream.writeUnsignedShort(tagNumber);
					dataOutputStream.writeUnsignedShort(attribute.format);
					dataOutputStream.writeInt(attribute.numberOfComponents);
					if (size > 4) {
						dataOutputStream.writeUnsignedInt(dataOffset);
						dataOffset += size;
					} else {
						dataOutputStream.write(attribute.bytes);
						// Fill zero up to 4 bytes
						if (size < 4) {
							for (int i = size; i < 4; ++i) {
								dataOutputStream.writeByte(0);
							}
						}
					}
				}

				// Write the next offset. It writes the offset of thumbnail IFD if there is one or
				// more tags in the thumbnail IFD when the current IFD is the primary image TIFF
				// IFD; Otherwise 0.
				if (ifdType == 0 && !mAttributes[IFD_TYPE_THUMBNAIL].isEmpty()) {
					dataOutputStream.writeUnsignedInt(ifdOffsets[IFD_TYPE_THUMBNAIL]);
				} else {
					dataOutputStream.writeUnsignedInt(0);
				}

				// Write values of data field exceeding 4 bytes after the next offset.
				for (Map.Entry<String, ExifAttribute> entry : mAttributes[ifdType].entrySet()) {
					ExifAttribute attribute = entry.getValue();

					if (attribute.bytes.length > 4) {
						dataOutputStream.write(attribute.bytes, 0, attribute.bytes.length);
					}
				}
			}
		}

		// Write thumbnail
		if (mHasThumbnail) {
			dataOutputStream.write(getThumbnailBytes());
		}

		// Reset the byte order to big endian in order to write remaining parts of the JPEG file.
		dataOutputStream.setByteOrder(ByteOrder.BIG_ENDIAN);

		return totalSize;
	}

	/**
	 * Determines the data format of EXIF entry value.
	 *
	 * @param entryValue The value to be determined.
	 * @return Returns two data formats gussed as a pair in integer. If there is no two candidate
	data formats for the given entry value, returns {@code -1} in the second of the pair.
	 */
	private static Pair<Integer, Integer> guessDataFormat(String entryValue) {
		// See TIFF 6.0 Section 2, "Image File Directory".
		// Take the first component if there are more than one component.
		if (entryValue.contains(",")) {
			String[] entryValues = entryValue.split(",", -1);
			Pair<Integer, Integer> dataFormat = guessDataFormat(entryValues[0]);
			if (dataFormat.first == IFD_FORMAT_STRING) {
				return dataFormat;
			}
			for (int i = 1; i < entryValues.length; ++i) {
				final Pair<Integer, Integer> guessDataFormat = guessDataFormat(entryValues[i]);
				int first = -1, second = -1;
				if (guessDataFormat.first.equals(dataFormat.first)
						|| guessDataFormat.second.equals(dataFormat.first)) {
					first = dataFormat.first;
				}
				if (dataFormat.second != -1 && (guessDataFormat.first.equals(dataFormat.second)
						|| guessDataFormat.second.equals(dataFormat.second))) {
					second = dataFormat.second;
				}
				if (first == -1 && second == -1) {
					return new Pair<>(IFD_FORMAT_STRING, -1);
				}
				if (first == -1) {
					dataFormat = new Pair<>(second, -1);
					continue;
				}
				if (second == -1) {
					dataFormat = new Pair<>(first, -1);
					continue;
				}
			}
			return dataFormat;
		}

		if (entryValue.contains("/")) {
			String[] rationalNumber = entryValue.split("/", -1);
			if (rationalNumber.length == 2) {
				try {
					long numerator = (long) Double.parseDouble(rationalNumber[0]);
					long denominator = (long) Double.parseDouble(rationalNumber[1]);
					if (numerator < 0L || denominator < 0L) {
						return new Pair<>(IFD_FORMAT_SRATIONAL, -1);
					}
					if (numerator > Integer.MAX_VALUE || denominator > Integer.MAX_VALUE) {
						return new Pair<>(IFD_FORMAT_URATIONAL, -1);
					}
					return new Pair<>(IFD_FORMAT_SRATIONAL, IFD_FORMAT_URATIONAL);
				} catch (NumberFormatException e)  {
					// Ignored
				}
			}
			return new Pair<>(IFD_FORMAT_STRING, -1);
		}
		try {
			Long longValue = Long.parseLong(entryValue);
			if (longValue >= 0 && longValue <= 65535) {
				return new Pair<>(IFD_FORMAT_USHORT, IFD_FORMAT_ULONG);
			}
			if (longValue < 0) {
				return new Pair<>(IFD_FORMAT_SLONG, -1);
			}
			return new Pair<>(IFD_FORMAT_ULONG, -1);
		} catch (NumberFormatException e) {
			// Ignored
		}
		try {
			Double.parseDouble(entryValue);
			return new Pair<>(IFD_FORMAT_DOUBLE, -1);
		} catch (NumberFormatException e) {
			// Ignored
		}
		return new Pair<>(IFD_FORMAT_STRING, -1);
	}

	// An input stream to parse EXIF data area, which can be written in either little or big endian
	// order.
	private static class ByteOrderedDataInputStream extends InputStream implements DataInput {
		private static final ByteOrder LITTLE_ENDIAN = ByteOrder.LITTLE_ENDIAN;
		private static final ByteOrder BIG_ENDIAN = ByteOrder.BIG_ENDIAN;

		private DataInputStream mDataInputStream;
		private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
		@SuppressWarnings("WeakerAccess") /* synthetic access */
		final int mLength;
		@SuppressWarnings("WeakerAccess") /* synthetic access */
				int mPosition;

		public ByteOrderedDataInputStream(InputStream in) throws IOException {
			mDataInputStream = new DataInputStream(in);
			mLength = mDataInputStream.available();
			mPosition = 0;
			mDataInputStream.mark(mLength);
		}

		public ByteOrderedDataInputStream(byte[] bytes) throws IOException {
			this(new ByteArrayInputStream(bytes));
		}

		public void setByteOrder(ByteOrder byteOrder) {
			mByteOrder = byteOrder;
		}

		public void seek(long byteCount) throws IOException {
			if (mPosition > byteCount) {
				mPosition = 0;
				mDataInputStream.reset();
				mDataInputStream.mark(mLength);
			} else {
				byteCount -= mPosition;
			}

			if (skipBytes((int) byteCount) != (int) byteCount) {
				throw new IOException("Couldn't seek up to the byteCount");
			}
		}

		public int peek() {
			return mPosition;
		}

		@Override
		public int available() throws IOException {
			return mDataInputStream.available();
		}

		@Override
		public int read() throws IOException {
			++mPosition;
			return mDataInputStream.read();
		}

		@Override
		public int read(byte[] b, int off, int len) throws IOException {
			int bytesRead = mDataInputStream.read(b, off, len);
			mPosition += bytesRead;
			return bytesRead;
		}

		@Override
		public int readUnsignedByte() throws IOException {
			++mPosition;
			return mDataInputStream.readUnsignedByte();
		}

		@Override
		public String readLine() throws IOException {
			logger.debug("Currently unsupported");
			return null;
		}

		@Override
		public boolean readBoolean() throws IOException {
			++mPosition;
			return mDataInputStream.readBoolean();
		}

		@Override
		public char readChar() throws IOException {
			mPosition += 2;
			return mDataInputStream.readChar();
		}

		@Override
		public String readUTF() throws IOException {
			mPosition += 2;
			return mDataInputStream.readUTF();
		}

		@Override
		public void readFully(byte[] buffer, int offset, int length) throws IOException {
			mPosition += length;
			if (mPosition > mLength) {
				throw new EOFException();
			}
			if (mDataInputStream.read(buffer, offset, length) != length) {
				throw new IOException("Couldn't read up to the length of buffer");
			}
		}

		@Override
		public void readFully(byte[] buffer) throws IOException {
			mPosition += buffer.length;
			if (mPosition > mLength) {
				throw new EOFException();
			}
			if (mDataInputStream.read(buffer, 0, buffer.length) != buffer.length) {
				throw new IOException("Couldn't read up to the length of buffer");
			}
		}

		@Override
		public byte readByte() throws IOException {
			++mPosition;
			if (mPosition > mLength) {
				throw new EOFException();
			}
			int ch = mDataInputStream.read();
			if (ch < 0) {
				throw new EOFException();
			}
			return (byte) ch;
		}

		@Override
		public short readShort() throws IOException {
			mPosition += 2;
			if (mPosition > mLength) {
				throw new EOFException();
			}
			int ch1 = mDataInputStream.read();
			int ch2 = mDataInputStream.read();
			if ((ch1 | ch2) < 0) {
				throw new EOFException();
			}
			if (mByteOrder == LITTLE_ENDIAN) {
				return (short) ((ch2 << 8) + (ch1));
			} else if (mByteOrder == BIG_ENDIAN) {
				return (short) ((ch1 << 8) + (ch2));
			}
			throw new IOException("Invalid byte order: " + mByteOrder);
		}

		@Override
		public int readInt() throws IOException {
			mPosition += 4;
			if (mPosition > mLength) {
				throw new EOFException();
			}
			int ch1 = mDataInputStream.read();
			int ch2 = mDataInputStream.read();
			int ch3 = mDataInputStream.read();
			int ch4 = mDataInputStream.read();
			if ((ch1 | ch2 | ch3 | ch4) < 0) {
				throw new EOFException();
			}
			if (mByteOrder == LITTLE_ENDIAN) {
				return ((ch4 << 24) + (ch3 << 16) + (ch2 << 8) + ch1);
			} else if (mByteOrder == BIG_ENDIAN) {
				return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
			}
			throw new IOException("Invalid byte order: " + mByteOrder);
		}

		@Override
		public int skipBytes(int byteCount) throws IOException {
			int totalSkip = Math.min(byteCount, mLength - mPosition);
			int skipped = 0;
			while (skipped < totalSkip) {
				skipped += mDataInputStream.skipBytes(totalSkip - skipped);
			}
			mPosition += skipped;
			return skipped;
		}

		@Override
		public int readUnsignedShort() throws IOException {
			mPosition += 2;
			if (mPosition > mLength) {
				throw new EOFException();
			}
			int ch1 = mDataInputStream.read();
			int ch2 = mDataInputStream.read();
			if ((ch1 | ch2) < 0) {
				throw new EOFException();
			}
			if (mByteOrder == LITTLE_ENDIAN) {
				return ((ch2 << 8) + (ch1));
			} else if (mByteOrder == BIG_ENDIAN) {
				return ((ch1 << 8) + (ch2));
			}
			throw new IOException("Invalid byte order: " + mByteOrder);
		}

		public long readUnsignedInt() throws IOException {
			return readInt() & 0xffffffffL;
		}

		@Override
		public long readLong() throws IOException {
			mPosition += 8;
			if (mPosition > mLength) {
				throw new EOFException();
			}
			int ch1 = mDataInputStream.read();
			int ch2 = mDataInputStream.read();
			int ch3 = mDataInputStream.read();
			int ch4 = mDataInputStream.read();
			int ch5 = mDataInputStream.read();
			int ch6 = mDataInputStream.read();
			int ch7 = mDataInputStream.read();
			int ch8 = mDataInputStream.read();
			if ((ch1 | ch2 | ch3 | ch4 | ch5 | ch6 | ch7 | ch8) < 0) {
				throw new EOFException();
			}
			if (mByteOrder == LITTLE_ENDIAN) {
				return (((long) ch8 << 56) + ((long) ch7 << 48) + ((long) ch6 << 40)
						+ ((long) ch5 << 32) + ((long) ch4 << 24) + ((long) ch3 << 16)
						+ ((long) ch2 << 8) + (long) ch1);
			} else if (mByteOrder == BIG_ENDIAN) {
				return (((long) ch1 << 56) + ((long) ch2 << 48) + ((long) ch3 << 40)
						+ ((long) ch4 << 32) + ((long) ch5 << 24) + ((long) ch6 << 16)
						+ ((long) ch7 << 8) + (long) ch8);
			}
			throw new IOException("Invalid byte order: " + mByteOrder);
		}

		@Override
		public float readFloat() throws IOException {
			return Float.intBitsToFloat(readInt());
		}

		@Override
		public double readDouble() throws IOException {
			return Double.longBitsToDouble(readLong());
		}
	}

	// An output stream to write EXIF data area, which can be written in either little or big endian
	// order.
	private static class ByteOrderedDataOutputStream extends FilterOutputStream {
		private final OutputStream mOutputStream;
		private ByteOrder mByteOrder;

		public ByteOrderedDataOutputStream(OutputStream out, ByteOrder byteOrder) {
			super(out);
			mOutputStream = out;
			mByteOrder = byteOrder;
		}

		public void setByteOrder(ByteOrder byteOrder) {
			mByteOrder = byteOrder;
		}

		@Override
		public void write(byte[] bytes) throws IOException {
			mOutputStream.write(bytes);
		}

		@Override
		public void write(byte[] bytes, int offset, int length) throws IOException {
			mOutputStream.write(bytes, offset, length);
		}

		public void writeByte(int val) throws IOException {
			mOutputStream.write(val);
		}

		public void writeShort(short val) throws IOException {
			if (mByteOrder == ByteOrder.LITTLE_ENDIAN) {
				mOutputStream.write((val >>> 0) & 0xFF);
				mOutputStream.write((val >>> 8) & 0xFF);
			} else if (mByteOrder == ByteOrder.BIG_ENDIAN) {
				mOutputStream.write((val >>> 8) & 0xFF);
				mOutputStream.write((val >>> 0) & 0xFF);
			}
		}

		public void writeInt(int val) throws IOException {
			if (mByteOrder == ByteOrder.LITTLE_ENDIAN) {
				mOutputStream.write((val >>> 0) & 0xFF);
				mOutputStream.write((val >>> 8) & 0xFF);
				mOutputStream.write((val >>> 16) & 0xFF);
				mOutputStream.write((val >>> 24) & 0xFF);
			} else if (mByteOrder == ByteOrder.BIG_ENDIAN) {
				mOutputStream.write((val >>> 24) & 0xFF);
				mOutputStream.write((val >>> 16) & 0xFF);
				mOutputStream.write((val >>> 8) & 0xFF);
				mOutputStream.write((val >>> 0) & 0xFF);
			}
		}

		public void writeUnsignedShort(int val) throws IOException {
			writeShort((short) val);
		}

		public void writeUnsignedInt(long val) throws IOException {
			writeInt((int) val);
		}
	}

	// Swaps image data based on image size
	private void swapBasedOnImageSize(@IfdType int firstIfdType, @IfdType int secondIfdType)
			throws IOException {
		if (mAttributes[firstIfdType].isEmpty() || mAttributes[secondIfdType].isEmpty()) {
			if (DEBUG) {
				logger.debug("Cannot perform swap since only one image data exists");
			}
			return;
		}

		ExifAttribute firstImageLengthAttribute =
				(ExifAttribute) mAttributes[firstIfdType].get(TAG_IMAGE_LENGTH);
		ExifAttribute firstImageWidthAttribute =
				(ExifAttribute) mAttributes[firstIfdType].get(TAG_IMAGE_WIDTH);
		ExifAttribute secondImageLengthAttribute =
				(ExifAttribute) mAttributes[secondIfdType].get(TAG_IMAGE_LENGTH);
		ExifAttribute secondImageWidthAttribute =
				(ExifAttribute) mAttributes[secondIfdType].get(TAG_IMAGE_WIDTH);

		if (firstImageLengthAttribute == null || firstImageWidthAttribute == null) {
			if (DEBUG) {
				logger.debug("First image does not contain valid size information");
			}
		} else if (secondImageLengthAttribute == null || secondImageWidthAttribute == null) {
			if (DEBUG) {
				logger.debug("Second image does not contain valid size information");
			}
		} else {
			int firstImageLengthValue = firstImageLengthAttribute.getIntValue(mExifByteOrder);
			int firstImageWidthValue = firstImageWidthAttribute.getIntValue(mExifByteOrder);
			int secondImageLengthValue = secondImageLengthAttribute.getIntValue(mExifByteOrder);
			int secondImageWidthValue = secondImageWidthAttribute.getIntValue(mExifByteOrder);

			if (firstImageLengthValue < secondImageLengthValue &&
					firstImageWidthValue < secondImageWidthValue) {
				HashMap<String, ExifAttribute> tempMap = mAttributes[firstIfdType];
				mAttributes[firstIfdType] = mAttributes[secondIfdType];
				mAttributes[secondIfdType] = tempMap;
			}
		}
	}

	/**
	 * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
	 */
	private static void closeQuietly(Closeable closeable) {
		if (closeable != null) {
			try {
				closeable.close();
			} catch (RuntimeException rethrown) {
				throw rethrown;
			} catch (Exception ignored) {
			}
		}
	}

	/**
	 * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
	 * Returns the total number of bytes transferred.
	 */
	private static int copy(InputStream in, OutputStream out) throws IOException {
		int total = 0;
		byte[] buffer = new byte[8192];
		int c;
		while ((c = in.read(buffer)) != -1) {
			total += c;
			out.write(buffer, 0, c);
		}
		return total;
	}

	/**
	 * Convert given int[] to long[]. If long[] is given, just return it.
	 * Return null for other types of input.
	 */
	private static long[] convertToLongArray(Object inputObj) {
		if (inputObj instanceof int[]) {
			int[] input = (int[]) inputObj;
			long[] result = new long[input.length];
			for (int i = 0; i < input.length; i++) {
				result[i] = input[i];
			}
			return result;
		} else if (inputObj instanceof long[]) {
			return (long[]) inputObj;
		}
		return null;
	}
}
